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.
Language-Specific Plugins
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
and 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
Along with read
and read-syntax
, the 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 reader
submodule:
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 Plugins
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 import
/export
system that is distinct from the traditional require
/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 #<void>
.
One thing we do want our plugin to do is add a new menu option for clippy:
1 |
(drracket:get/extend:extend-unit-frame clippy-frame-mixin) |
Where we define clippy-frame-mixin
as:4
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.
Closing Remarks
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.