Quickscript, a scripting plugin for DrRacket
Laurent Orseau <firstname.lastname@example.org>
|(require quickscript)||package: quickscript|
Quickscript’s makes it easy to extend DrRacket with small Racket scripts to automate some actions in the editor, while avoiding the need to restart DrRacket.
Creating a new script is as easy as a click on Scripts | New script…. Each script is automatically added as an item to the Scripts menu, without needing to restart DrRacket. A keyboard shortcut can be assigned to a script (via the menu item). By default, a script takes as input the currently selected text, and outputs the replacement text. There is also direct access to some elements of DrRacket GUI for advanced scripting, like DrRacket’s frame and the definition or interaction editor.
Quickscript is installed automatically with DrRacket, so you don’t need to do anything.
2.1 Installing scripts: Quickscript Extra
You can use Quickscript on its own, but the Quickscript Extra package has a wide range of useful scripts as well as some example scripts intended for customisation by the user.
Then click on Scripts|Manage|Compile scripts. (There is no need to restart DrRacket.)
2.2 Installing scripts: More scripts
More scripts can be found on the
3 Make your own script: First simple example
Click on the Scripts|Manage|New script… menu item, and enter Reverse for the script name. This creates and opens the file reverse.rkt in the user’s scripts directory. Also, a new item automatically appears in the Scripts menu.
Don’t name your script function reverse, it would shadow Racket’s own and make the script hang.
If you later change the #:label property, you will need to reload the menu by clicking on Scripts|Manage|Reload menu after saving the file).
(define-script reverse-selection #:label "Reverse" (λ (selection) (list->string (reverse (string->list selection)))))
Then go to a new tab, type some text, select it, and click on Scripts|Reverse, and voilà!
4 Into more details
Quickscript adds a Scripts menu to the main DrRacket window. This menu has several items, followed by the list of scripts.
The New script item asks for a script name and creates a corresponding .rkt file in the user’s script directory, and opens it in DrRacket.
(define-script a-complete-script ; Properties: #:label "Full script" #:help-string "A complete script showing all properties and arguments" #:menu-path ("Submenu" "Subsubmenu") #:shortcut #\a #:shortcut-prefix (ctl shift) #:output-to selection #:persistent #:os-types (unix macosx windows) ; Procedure with its arguments: (λ (selection #:frame fr #:editor ed #:definitions defs #:interactions ints #:file f) "Hello world!"))
Note that the arguments of the properties are literals, not expressions, so they must not be quoted. Below we detail first the procedure and its arguments and then the script’s properties.
(define-script name property ... proc)
property = #:label label-string | #:help-string string | #:menu-path (label-string ...) | #:shortcut char | symbol | #f | #:shortcut-prefix (shortcut-prefix ...) | #:persistent? #t | #f | #:output-to output-to | #:os-types (os-type ...) shortcut-prefix = alt | cmd | meta | ctl | shift | option output-to = selection | new-tab | message-box | clipboard | #f os-type = macosx | unix | windows proc =
(λ (selection-id [#:editor editor-id] [#:definitions definitions-id] [#:interactions interactions-id] [#:frame frame-id] [#:file file-id]) body-expr ... return-expr)
Observe again that the arguments of the properties are literals and not expressions. This is because the script file is read twice for different purposes. The first time, Quickscript reads the script file to extract the minimum information necessary to build the menu items in DrRacket. No Racket operation is performed at this stage so as to be as light and quick as possible. Then, when the corresponding menu item is clicked, Quickscript reads the script file a second time, this time to actually read and visit the Racket module and call the corresponding procedure. That is, the script modules are instantiated only on demand to reduce the loading time and memory footprint.
4.1 The script’s procedure
When clicking on a script label in the Scripts menu in DrRacket, its corresponding procedure is called. The procedure takes at least the selection argument, which is the string that is currently selected in the current editor. The procedure must returns either #f or a string?. If it returns #f, no change is applied to the current editor, but if it returns a string, then the current selection is replace with the return value.
The path to the current file of the definition window, or #f if there is no such file (i.e., unsaved editor).Example:
(define-script current-file-example #:label "Current file example" #:output-to message-box (λ (selection #:file f) (string-append "File: " (if f (path->string f) "no-file") "\nSelection: " selection)))
#:definitions : text%
#:interactions : text%
The text% editor of the current interaction window. Similar to #:definitions.
#:editor : text%
The text% current editor, either the definition or the interaction editor. Similar to #:definitions.
#:frame : drracket:unit:frame<%>
DrRacket’s frame. For advanced scripting.Example:
(define-script append-plop #:label "Append plop" (λ (selection [more ""] #:even-more [even-more ""]) (string-append selection "_plop" more even-more))) (define-script append-plop-plip #:label "Append plop plip ploop" (λ (selection) ; Call the first script's procedure: (append-plop selection "_plip" #:even-more "_ploop")))
4.2 The script’s properties
The properties are mere data and cannot contain expressions.
Most properties (#:label, #:shortcut, #:shortcut-prefix, #:help-string) are the same as for the menu-item% constructor. In particular, a keyboard shortcut can be assigned to an item.
If a property does not appear in the dictionary, it takes its default value.
Note that different scripts in different files can share the same submenus.
If selection, the output of the procedure replaces the selection in the current editor (definitions or interactions), or insert the output at the cursor if there is no selection. If new-tab, the return value is written in a new tab. If message-box, the return value (if a string) is displayed in a message-box. If clipboard, the return value (if a string) is copied to the clipboard. If #f, the return value is not used.
If this value is changed, make sure to reload the menu with Scripts|Manage|Reload menu.
If they keyword #:persistent is not provided, each invocation of the script is done in a fresh namespace.
But if #:persistent is provided, a fresh namespace is created only the first time it is invoked, and the same namespace is re-used for the subsequent invocations. Note that a single namespace is kept per file, so if different scripts in the same file are marked as persistent, they will all share the same namespace (and, thus, variables). Also note that a script marked as non-persistent will not share the same namespace as the other scripts of the same file marked as persistent.Consider the following script:
(define count 0) (define-script persistent-counter #:label "Persistent counter" #:persistent #:output-to message-box (λ (selection) (set! count (+ count 1)) (number->string count)))
If the script is persistent, the counter increases at each invocation of the script via the menu, whereas it always displays 1 if the script is not persistent.
Note: Persistent scripts can be stopped and reset by clicking on the Scripts|Manage|Stop persistent scripts menu item. In the previous example, this will reset the counter. Make sure to stop a persistent script after editing it. Scripts|Manage|Reload menu and Scripts|Manage|Compile scripts also stop persistent scripts.
Technical point: The script’s procedure is called outside of the namespace that was used to dynamic-require it, and inside DrRacket frame’s namespace so as to have access to objects in this frame.
This keyword must be followed by a list of supported os-types. Defaults to all types, i.e. (unix macosx windows).
If changes are made to these properties, the Scripts menu will probably need to be reloaded by clicking on Scripts|Manage|Reload menu.
A script function defined with define-script always adds a menu item, and is called only when the menu item is clicked or called.
By contrast, script functions defined with define-hook do not add a menu item, but are run
automatically on specific events —
(define-hook name property ... proc)
property = #:help-string string | #:persistent? #t | #f | #:os-types (os-type ...) os-type = macosx | unix | windows proc =
(λ ([#:editor editor-id] [#:definitions definitions-id] [#:interactions interactions-id] [#:frame frame-id] [#:file file-id] other-kwargs ...) body-expr ... return-expr)
See define-script for information regarding the keyword arguments of the script function, and the properties of the script. Note that a hook function does not have a selection-id argument. Each hook may receive additional optional arguments in other-kwargs, but as for scripts, these arguments are optional and do not need to be specified in the hook function’s signature: Quickscript recognizes which keywords are asked for by the hook.
The additional keywords accepted by the hook function are the arguments of the original method or function.
(define-hook after-load-file (λ (#:file f #:in-new-tab? new-tab?) (message-box "on-load-file" (format "f: ~a\n new-tab?: ~a" f new-tab?))))
DrRacket’s frame is always available via the #:frame keyword.
Note: While scripts default keyword arguments always correspond to current tab (the one in focus), hooks may be called on other tabs.
after-load-file (#:in-new-tab?) : called after a file is loaded in an existing tab or in a new tab.
on-save-file (#:save-filename #:format) : called before the file is saved.
after-save-file () : called after a file is saved.
after-create-new-tab () : called when a new tab is created.
on-tab-change (#:tab-from #:tab-to) : called when the keyboard focus changes from #:tab-from to #:tab-to.
on-tab-close (#:tab) : called before the tab is closed.
on-startup () : called when DrRacket starts, but before the frame is shown.
after-create-new-drracket-frame (#:show): called after a new DrRacket frame is created.
on-close () : called when a DrRacket frame is closed.
6 Script library
When the user creates a new script, the latter is placed into a sub-directory of (find-system-path 'pref-dir). A direct access to this folder is provided via the Scripts|Manage|Open script… menu entry.
Additional directories to look for scripts can be added via the Scripts|Manage|Library menu entry. When a directory is added to the library, all its .rkt files (non-recursively) are considered as scripts. Specific files can be excluded from the library.
7 Shadow scripts
When a script is installed from a third party package (like quickscript-extra), it comes with its set of own values for its properties. These values may not suit the user who may want to redefine some of them, like the menu path or the keyboard shortcuts. An obvious choice for the user is to copy/paste the entire script, but this would prevent from benefiting from further bug fixes and enhancements made by the writer of the original script.
To solve this problem, the user can instead make a shadow script, which creates a new script in the user’s directory, with its own set of properties that can be changed by the user, but the procedure of this script is bound to that of the original script.
To make a shadow script, open the script library in Scripts|Manage|Library, navigate to the third-party script and click on Shadow.
8 Updating the quickscript package
To update Quickscript once already installed, either do so through the File|Package Manager menu in DrRacket, or run raco pkg update quickscript.
The user’s scripts will not be modified in the process.
9 Distributing your own scripts
The simplest way to distribute a small script s to publish it as a gist or on PasteRack, and share the link. A user can then copy/paste the contents into a new script. Don’t forget to include a permissive license such as MIT/Apache 2.
If the file "register.rkt" is not at the root, the runtime-path needs to be modified accordingly.
#lang racket/base (require (for-syntax racket/base racket/runtime-path (only-in quickscript/library add-third-party-script-directory!))) ;; This file is going to be called during setup and will automatically ;; register the scripts subdirectory in quickscript's library. (begin-for-syntax (define-runtime-path script-dir "scripts") (add-third-party-script-directory! script-dir))
You can see an example with quickscript-extra.
Don’t forget to register your package on the Racket server.
Apache-2.0 or MIT License, at your option.
Copyright (c) 2012-2023 by Laurent Orseau <email@example.com>.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.