Throughout these tutorials, we’ve been relying on DrRacket as our primary tool for editing and testing code. We don’t use DrRacket because it’s mandatory—we could use any text editor. Rather, we use it because it offers a lot of extra conveniences: the REPL, background syntax checking, indenting, syntax coloring, and toolbar buttons.
But these conveniences aren’t just available to the main Racket language and its dialects. Keeping with the spirit of Racket as a platform for building languages, the extra conveniences offered by DrRacket are available to any Racket-implemented language.
For those writing a language, this is great news. We probably weren’t going to write a graphical IDE from scratch. But with a little extra effort, we can integrate our language with DrRacket.
In this section, we’ll see how this works by updating jsonic with an indenter, syntax colorer, and toolbar buttons that work in DrRacket.
We’ve already learned that when we invoke a language—for instance, #lang jsonic—Racket starts by looking for a "main.rkt" module within the jsonic directory. Within that module, it expects to find a reader submodule that provides our read-syntax function. Thus our "main.rkt" for jsonic currently looks like this:
1 2 3 4 | #lang br/quicklang (module reader br (require "reader.rkt") (provide read-syntax)) |
Similarly, when we invoke a language in DrRacket (as opposed to using racket on the command line), DrRacket looks for a get-info function in the same place as read-syntax—that is, in the reader submodule of "main.rkt".
DrRacket uses the get-info function to determine if the language supports certain features within DrRacket, and as a way of dispatching certain tasks to functions we’ll provide. Without get-info, DrRacket resorts to its default settings. Thus, unlike read-syntax, get-info is always optional—a language will run fine without it. + It’s called get-info and not get-drracket-info because tools other than DrRacket are allowed to use it too.
We’ll add our get-info directly to our reader submodule in "main.rkt":
1 2 3 4 5 6 7 | #lang br/quicklang (module reader br (require "reader.rkt") (provide read-syntax get-info) (define (get-info port src-mod src-line src-col src-pos) ···)) |
DrRacket calls get-info with five arguments: an input port and four source-location fields: src-mod (the name of the module invoking get-info), plus src-line, src-col, and src-pos. DrRacket passes these arguments in case we want to use them to adjust how our language integrates with DrRacket. In this case, we don’t. So having acknowledged that they exist, we’ll move on. + Curious characters can read more about the get-info arguments in the Racket docs.
The result of get-info needs to be another function, which we’ll call handle-query:
1 2 3 4 5 6 7 8 9 10 11 12 13 | #lang br/quicklang (module reader br (require "reader.rkt") (provide read-syntax get-info) (define (get-info port src-mod src-line src-col src-pos) (define (handle-query key default) (case key [(color-lexer) ···] [(drracket:indentation) ···] [(drracket:toolbar-buttons) ···] [else default])) handle-query)) |
DrRacket will call this new function with two arguments: a query key representing a request for a certain kind of information, and a default value that we should return if we don’t handle that key. The three key values we’ll handle are color-lexer (for syntax coloring), drracket:indentation, and drracket:toolbar-buttons. Inside the function, we’ll branch on key using case, with a branch for each supported key, and return default in the else branch.
For each of our three supported keys, we’ll return a function that handles that task for DrRacket:
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 src-mod src-line src-col src-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)) |
These functions do different things, so they’ll have different kinds of input and output. We’ll cover those details in the next sections.
Here in get-info, we just have to import the modules that export those functions, and use them as return values of handle-query. So let’s imagine that we have three new modules in jsonic: "colorer.rkt" that provides color-jsonic, "indenter.rkt" that provides indent-jsonic, and "buttons.rkt" that provides button-list.
These functions are only needed when we use jsonic in DrRacket, and otherwise unnecessary. Therefore, we’d like to ignore them by default, but make them available to DrRacket when it requests them. To do this, we’ll use a variant of require called dynamic-require. As the name suggests, dynamic-require lets us import a function from a module at run time rather than compile time. A dynamic-require expression has the quoted name of the module followed by the quoted name of the function: + We quote them so they’re treated as literal names at run time. If they weren’t quoted, they’d be treated as variables.
1 | (dynamic-require 'jsonic/colorer 'color-jsonic) |
For now, since we still have to make these new modules and functions, let’s comment out the top three branches of the case statement, which we can do by prefixing each branch with #;:
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 src-mod src-line src-col src-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)) |
As we add each of these modules in the next three sections, we’ll uncomment the corresponding branch above.