At the top of every DrRacket editor window is a toolbar with buttons:
For jsonic, a useful button might be one that inserts our expression delimiters @$ ··· $@ and automatically moves the cursor between them.
So let’s add that.
We already set up our "jsonic/main.rkt" file to work with toolbar buttons. Let’s remind ourselves where we left it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
#lang br/quicklang (module reader br (require "reader.rkt") (provide read-syntax get-info) (define (get-info port mod line col pos) (define (handle-query key default) (case key [(color-lexer) (dynamic-require 'jsonic/colorer 'color-jsonic)] [(drracket:indentation) (dynamic-require 'jsonic/indenter 'indent-jsonic)] [(drracket:toolbar-buttons) (dynamic-require 'jsonic/buttons 'button-list)] [else default])) handle-query))
When DrRacket notices that jsonic is being used in a code editor, it will call our get-info function with a drracket:toolbar-buttons query. We’ll return a list of buttons that DrRacket will install in the toolbar. Simple.
As "jsonic/main.rkt" indicates, our list of buttons will be called button-list and live in a module called "jsonic/buttons.rkt". So let’s start that file now:
Our button-list is, of course, a list. Each button within this list is itself a list of four values:
A string representing the button label shown in the toolbar.
A 16-pixel-high bitmap representing the icon that goes next to the label.
A function that’s called when the button is pressed. This function receives a single argument: a reference to the editor window where the button was pressed.
A number that determines the ordering of the button on the toolbar. If we don’t care about this, we can set it to #f.
We’re only going to have one button called our-jsonic-button, but we could have more.
Let’s use Insert expression as the button label, and #f as our ordering number. That takes care of two out of four fields:
1 2 3 4 5 6 7 8 9 10 11 12
But the most important part of the button is the button-press function. We’ll define a new function called button-func and add it as the third value in our button:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
#lang br (require racket/draw) (define (button-func drr-window) (define expr-string "@$ $@") (define editor (send drr-window get-definitions-text)) (send editor insert expr-string) (define pos (send editor get-start-position)) (send editor set-position (- pos 3))) (define our-jsonic-button (list "Insert expression" (make-object bitmap% 16 16) button-func #f)) (provide button-list) (define button-list (list our-jsonic-button))
Let’s walk through the lines of this function.
Our single argument drr-window is a reference to the current DrRacket window. (We can skip writing a contract for this function because it’ll only be used by DrRacket, which can be trusted to pass the correct kind of argument.)
The drr-window object is an instance of the frame% class in Racket’s GUI library (similar to how the text box in the indenter was an instance of the text% class). Because DrRacket passes every button a reference to the whole window, a button can do pretty much anything to the window: change the text, draw on the screen, and so on. But that also means that writing button functions requires some familiarity with the GUI library.
To avoid that detour, we’ll use a little extra handwaving as we walk through the code above, which inserts our expression delimiters separated by two spaces @$ $@, and then moves the insertion point into the middle.
To use methods stored in Racket objects, we send an object a method name and any necessary arguments. To start, we grab a reference to the upper pane of the window containing the source with get-definitions-text, and store it in editor. We then insert our expr-string in the editor. By default, it will go wherever the insertion point is positioned, just the same as if we typed it.
After the insertion, the insertion point will end up at the end of the expr-string. But we want it to be in the middle of the inserted text. So first, we get-start-position to find out where the insertion point is. Then we subtract 3 (because our expr-string is 6 characters long) and pass this adjusted position to set-position, which will move the insertion point to the right place.
It’s not a miracle of software engineering. But it will work.
At this point it’s important to select the menu item Racket → Reload #lang Extensions to try out the changes. While this usually isn’t necessary when working on a language, DrRacket caches the results of the get-info function the first time it invokes a language. So if we’ve already been fiddling with #lang jsonic, we won’t see the new button until we reload the language, which forces a fresh call to get-info.
Once we do, our new button will show up on the left edge of the toolbar:
Then, if we select some code in the window:
And click the Insert expression button:
The selected code will be replaced with our expression delimiters, and the insertion point moved within: