Making an IDE Plugin for DrRacket
I recently had several students ask me to show them how to make DrRacket plugins for their new language. It is easy to do, but I noticed that there aren any existing guides on how to do it. There is the plugin documentation, which is a good reference and has some good examples. Unfortunately, it lacks a good step by step tutorial on how to make new IDE plugins. This post is that tutorial.
DrRacket plugins fall into one of two main categories:
- Language-Specific Plugins
- Global Plugins
The former only enables the plugin when the user is editing a file in that plugin’s associated language. The latter, however, is for plugins that should always be enabled, such as DrRacket’s Vim Mode. This tutorial covers both plugin styles.
Before you can create a language-specific plugin for DrRacket, you first need a language that Racket can recognize. There are plenty of good resources to learn how to make languages with Racket. I recommend reading Languages as Dotfiles, which is a simple step by step tutorial. Other good resources are Matthew Butterick’s Beautiful Racket and the Racket documentation.
Making a DSL
For this tutorial we will assume a language called
clippy, the happy-helping language. The language will be identical to
racket/base, except that it puts a button in DrRacket’s toolbar that displays “Howdy!” when clicked. Let’s start out with the following program for our language:
1 2 3 4 5 6
#lang racket/base ;; clippy/main.rkt (provide (all-from-out racket/base)) (module* reader syntax/module-reader clippy #:read read #:read-syntax read-syntax)
This file re-exports
racket/base, and also sets the reader to use
read-syntax. This file assumes that it has been installed, so make sure at some point to run:
$ cd clippy/ $ raco pkg install
Now you can open DrRacket and start a file with
#lang clippy and DrRacket will behave as if you told it to use
racket/base for its language.
Adding Language-Specific Plugins
reader submodule optionally provide an info function for tools surrounding your DSL. This info function includes everything from how syntax should be colored to even changing the entire behavior of the IDE. Language info functions are given a key, and responds with a value based on a pre-determined set of rules. DrRacket also gives default values to this function to handle different keys. The Racket documentation contains a list of every possible symbol DrRacket will use.
For this tutorial, we only care about one possible key:
drracket:toolbar-buttons. This key tells DrRacket to add new buttons when editing a specific language. DrRacket expects this key to contain a list for each button, which in tern is represented as a list with an element for:
- the button’s name,
- the button’s image,
- the callback when the button is pressed, and
- the button’s priority in the toolbar.
For simplicity, let’s define our list in its own file:
1 2 3 4 5 6 7 8 9 10 11
Now we are ready to create our
make-info function, and place it in the
1 2 3 4 5
(define (make-info key default use-default) (case key [(drracket:toolbar-buttons) (list (dynamic-require 'clippy/button 'clippy-button))] [else (use-default key default)]))
Note the use of
dynamic-require rather than a static
require statement. The
clippy-button list requires the
pict library to draw a circle. However, the
clippy language proper does not have that dependency. By using a dynamic-require, we only load the
pict library for DrRacket.1
Putting everything together gives:
1 2 3 4 5 6 7 8 9 10 11 12 13
#lang racket/base ;; clippy/main.rkt (provide (all-from-out racket/base)) (module* reader syntax/module-reader clippy #:read read #:read-syntax read-syntax #:info make-info (define (make-info key default use-default) (case key [(drracket:toolbar-buttons) (list (dynamic-require 'clippy/button 'clippy-button))] [else (use-default key default)])))
Restart DrRake, and create a file that begins with
#lang clippy. A new button should appear near the run button.
Global DrRacket plugins require the IDE to have meta-information about installed languages. Racket collections can optionally use an
info.rkt file to store meta information for the package, e.g., its name, version, dependancies, etc. In this case, we will use our
info.rkt file to inform DrRacket that
tool.rkt provides a DrRacket plugin. We will also give it the name
"Clippy", and add no icons:
1 2 3 4 5 6
DrRacket expects its plugin file to export a single identifier
tool@, which is a unit2, that satisfies the
drracket:tool-exports^ signature. We could build and provide this unit from any file, or we could use the
racket/unit DSL. This DSL implicitly puts the body of the module3 in a unit, and provides that unit as the file name. Using
racket/unit, we will make a file that looks like:
1 2 3 4 5 6 7 8 9 10
Let’s break this file down piece by piece. First, the top-level require:
Files written in
racket/unit are implicitly wrapped in the body of a unit. However,
require statements must be at the module level. Therefore, the
racket/unit language allows for one require form at the top of the file to bring in everything. In this case we are bringing in
drracket/tool. We bring in this library for the next two lines:
Units, unlike Racket modules, can have circular dependancies. Consequently, they use their own
export system that is distinct from the traditional
provide system. Here, we are giving our unit access to the
drracket:tool^ signature, and specifying that our unit satisfies the
drracket:tool-exports^ signature. This signature requires us to provide two functions:
Both of these functions are called at different points during DrRacket’s startup. Our plugin, however, does not need to set up any special state during these phases. Therefore, we define both functions to do nothing and return
One thing we do want our plugin to do is add a new menu option for clippy:
Where we define
1 2 3 4 5 6 7 8 9 10
This mixin adds a clippy button to the insert menu, and when selected, a new message box pops up for clippy to say “Need any help?”.
Putting the whole file together gives us:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
#lang racket/unit ;; clippy/tool.rkt (require drracket/tool racket/class racket/gui/base) (import drracket:tool^) (export drracket:tool-exports^) (define (phase1) (void)) (define (phase2) (void)) (define clippy-frame-mixin (mixin (drracket:unit:frame<%>) () (super-new) (inherit get-insert-menu) (new menu-item% [parent (get-insert-menu)] [label "Paperclip Help"] [callback (λ (i e) (message-box "Clippy" "Need any help?"))]))) (drracket:get/extend:extend-unit-frame clippy-frame-mixin)
As before, remember to restart DrRacket. When you do, there will now be a new menu option in the insert menu. When you press it, a helpful dialog box appears.
This post shows you how to make plugins for the DrRacket IDE. Both language-specific plugins, as well as global plugins. You can find the project that the samples in this blog are based on in the video source code. You can also look at a 3rd party plugin framework for DrRacket, which lets you play with plugins without needing to restart DrRacket.