Skip to content

Instantly share code, notes, and snippets.

@sliminality
Last active October 8, 2023 04:47
Show Gist options
  • Star 29 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save sliminality/3f1527f8e910c36b3303346422b03409 to your computer and use it in GitHub Desktop.
Save sliminality/3f1527f8e910c36b3303346422b03409 to your computer and use it in GitHub Desktop.
Racket and Sublime Text 3

Racket and Sublime Text 3: A Guide for People With Low Standards

This gist documents my findings from a year of intermittent efforts to use Sublime Text 3 for Racket programming. Everything in this article is probably a Racket or Sublime worst practice: in any config situation, my goal is "spend as little time as possible for the greatest possible improvement to my user experience." If you are actually proficient in either of the relevant technologies, spare yourself and stop reading (and then come back and tell me how to do this correctly).

This guide is intended for people like myself, who:

  • Desire a respectable scrolling framerate, or have some other DrRacket grievance significant enough to justify giving up all of the included batteries, and
  • Inexplicably need to write Racket and related dialects frequently enough to justify all of this, yet not at a level of substance demanding of an actually reliable workflow (so basically EECS 111 teaching assistants only), and
  • Somehow still don't know the keybindings for any serious editors, and would rather yak shave than learn the aforementioned keybindings, due to some combination of laziness and a fixed mindset. (UPDATE: I now use Vim as my primary editor, but it's not a whole lot better for Racket.)

The configuration process consists of the following parts:

  1. Getting syntax highlighting to kinda work
  2. Getting Sublime to stop inserting extra quotes ' everywhere
  3. Getting indentation to mostly work
  4. Getting a REPL kind of
  5. Miscellaneous keybindings

Prerequisites

  • You have Package Control, and know how to install and remove packages.
  • You know how to edit extension files, either:
    • by editing the original extension files (stored in ~/Library/Application Support/Sublime Text 3/Packages/ on macOS Sierra) with PackageResourceViewer or a similar extension, or
    • by creating a new folder in your User directory (~/Library/Application Support/Sublime Text 3/Packages/User/ on macOS Sierra) with the name of the extension, and then creating a new file at the same relative path as in the original extension files.
  • You know how to add new keybindings, either:
    • by searching for the Preferences: Key Bindings option in the Command Palette, or
    • by navigating to Sublime Text > Preferences > Key Bindings in the menu bar.

Syntax Highlighting

Install the Racket package using Package Control. In theory, this adds a Racket syntax definition and the corresponding highlighting.

Of course, reality sucks and the provided syntax file is still incomplete, so replace the extension's Racket.tmLanguage file (using one of the methods explained in the "Prerequisites" section) with the Racket.tmLanguage file attached in this gist, which adds support for more boolean forms and keywords/standard library functions.

Note that this only recognizes primitives for the Racket language, not necessarily all of the primitives for the Student Languages or PLAI.

Stop Autocompleting Quotes

In Racket, ' is an alias for the quote syntactic form. In basically every other language, ' delimits a string, so Sublime will try to autocomplete this by adding another quote every time you try to define a new list.

To fix this, we're going to overload the keybinding for ' to insert a snippet when triggered from the Racket source scope. This "snippet" will consist of exactly one character: '. Great times.

Add this to your keybindings file (see the instructions in "Prerequisites"):

{
  // ...
  { "keys": ["'"], "command": "insert_snippet", "args": { "contents": "'" }, "context": [
      { "key": "selector", "operator": "equal", "operand": "source.racket" }
    ]
  },
  // ...
}

Indentation

One of DrRacket's best features is smart indentation for function arguments.

Install lispindent from Package Control. This package actually works for the most part, with the caveat that your file must be saved to disk with a .rkt extension for indentation to work correctly.

With the lispindent plugin, you get the Cmd + I shortcut to indent the whole file, just like in DrRacket.

In addition, it may be helpful to configure Sublime such that highlighting a region of text and hitting Tab will reindent that region correctly. To do so, edit your keybindings file (by searching for the Preferences: Key Bindings option in the Command Palette, or Sublime Text > Preferences > Key Bindings in the menu) to add the following:

{
  ...
  { "keys": ["tab"], "command": "lispindent", "context": [
      { "key": "setting.auto_indent", "operator": "equal", "operand": true },
      { "key": "selection_empty", "operator": "equal", "operand": false, "match_all": true },
      { "key": "selector", "operator": "equal", "operand": "source.racket" }
    ]
  },
  ...
}

Advanced Configuration

Some Racket dialects introduce new forms which should not be indented like functions (can you tell I don't actually know any technical terminology). For example, consider the define keyword:

;; RIGHT
(define (foo x)
  (+ x 1))

;; WRONG
(define (foo x)
        (+ x 1))

If you need to add such a form, like define-syntax-type, edit the lispindent extension's lispindent.sublime-settings file, specifically by adding to the following section:

"racket": {
  "detect": ".*\\.(rkt|rktl|rktd)$",
  "default_indent": "function",
  "regex":
  ["(define:?|define-predicate|define-struct/exec|define-struct:|define-type|",
   "define-typed-struct|define-syntax-rule|local|match-define|pdefine:|struct:?|",

   // ADD MORE STUFF TO THIS REGEX, delimiting terms with pipes, e.g. "define-syntax-type|"

   "call-with-input-file|call-with-input-file\\*|call-with-output-file|case|cases|catch|",
   "class|class\\*|class\\*/names|class-asi|class100|class100\\*|class100\\*/names|class100-asi|",
   // ...

REPL

Install SublimeREPL from Package Control.

I'm pretty sure this one works straight out of the box, too.

  • To open a new Racket REPL, search in the Command Palette for SublimeREPL: Racket.
  • To send the current file to the REPL, use the ctrl+,, f shortcut, which basically looks like holding down ctrl, holding down , and then releasing ctrl to hit f.

Now you should be able to open a split pane using Cmd + Option + Shift + 2 or View > Layout > Rows: 2, open a Racket REPL in the bottom pane, and use this enormous "shortcut" to run your file in the REPL kind of like in DrRacket:

knockoff Racket REPL

Note that the REPL doesn't support #lang, so you are kind of out of luck for many projects, but you can still edit in Sublime and build in the terminal or with a DrRacket tests file that requires the file you're editing.

Getting Cmd + R to Work

If this keybinding is a pain, you can overload Cmd + R to run the current file in an open REPL, just like in DrRacket. (Note that this will overload the existing binding for Cmd + R, which searches for all the symbols in a project.)

First, create a new file in your User directory called EvalInREPL.sublime-macro:

[
  {
    "args":
    {
      "scope": "file"
    },
    "command": "repl_transfer_current"
  }
]

Next, add the following to your keybindings file:

{
  // ...
  { "keys": ["super+r"], "command": "run_macro_file", "args": { "file": "Packages/User/EvalInREPL.sublime-macro" },
    "context": [
      { "key": "selector", "operator": "equal", "operand": "source.racket" },
    ]
  },
  // ...
}

Useful Key Bindings for S-Expressions

Remember that Ctrl + M jumps your cursor to the nearest bracket, and hitting Ctrl + M again jumps you to its pair.

Additionally, Ctrl + Shift + M will select the contents of the enclosing brackets, and hitting Ctrl + Shift + M again will expand the selection to include the enclosing brackets themselves. This is really useful for swapping S-expressions #substitutionprinciple

Conclusion

Congratulations, you should now have the barest approximation of a Racket editing environment in Sublime. You will probably recuperate the lost time in UI responsiveness alone.

Various parts of this setup will inevitably break for complex projects (e.g. whenever you need a #lang statement you can't use the REPL, many #lang plai constructs aren't going to be syntax-highlighted, etc.) so your best bet is to go back to using the terminal or DrRacket for complex situations. However, if you're writing core Racket and not executing very often, this setup is fairly usable.

For reference, I've attached three files:

  1. The expanded Racket.tmLanguage syntax definition mentioned in the first section, and
  2. An example Default (OSX).sublime-keymap keybindings file containing all of the custom bindings discussed in this guide, and
  3. The EvalInREPL.sublime-macro file required to enable the Cmd + R keybinding for SublimeREPL.
{
// Evaluate file in the open SublimeREPL.
// Depends on you creating the Packages/User/EvalInREPL.sublime-macro file.
{ "keys": ["super+r"], "command": "run_macro_file", "args": { "file": "Packages/User/EvalInREPL.sublime-macro" },
"context": [
{ "key": "selector", "operator": "equal", "operand": "source.elm" }
]
},
// Reindent selection on tab
{ "keys": ["tab"], "command": "lispindent", "context": [
{ "key": "setting.auto_indent", "operator": "equal", "operand": true },
{ "key": "selection_empty", "operator": "equal", "operand": false, "match_all": true },
{ "key": "selector", "operator": "equal", "operand": "source.racket" }
]
},
// Stop autocompleting single quotes
{ "keys": ["'"], "command": "insert_snippet", "args": { "contents": "'" }, "context": [
{ "key": "selector", "operator": "equal", "operand": "source.racket" }
]
}
}
[
{
"args":
{
"scope": "file"
},
"command": "repl_transfer_current"
}
]
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>fileTypes</key>
<array>
<string>rkt</string>
</array>
<key>name</key>
<string>Racket</string>
<key>patterns</key>
<array>
<dict>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>string.quoted.double.source.racket</string>
</dict>
</dict>
<key>match</key>
<string>[^\\](\"[^\"]*\")</string>
</dict>
<dict>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>keyword.source.racket</string>
</dict>
<key>2</key>
<dict>
<key>name</key>
<string>entity.name.variable.source.racket</string>
</dict>
</dict>
<key>match</key>
<string>\((define)\s+([a-zA-Z0-9_\-?\+^:/!]+)\s*</string>
<key>name</key>
<string>meta.variable.source.racket</string>
</dict>
<dict>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>keyword.source.racket</string>
</dict>
<key>2</key>
<dict>
<key>name</key>
<string>entity.name.function</string>
</dict>
</dict>
<key>match</key>
<string>\((define)\s+\(([a-zA-Z0-9_\-?\+^:/!]+)\s*</string>
<key>name</key>
<string>meta.function.source.racket</string>
</dict>
<dict>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>keyword.source.racket</string>
</dict>
<key>2</key>
<dict>
<key>name</key>
<string>entity.name.type</string>
</dict>
</dict>
<key>match</key>
<string>\((struct)\s+([a-zA-Z0-9_\-?\+^]+)\s+</string>
<key>name</key>
<string>meta.struct.source.racket</string>
</dict>
<dict>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>keyword.source.racket</string>
</dict>
</dict>
<key>match</key>
<string>[\s\(](if|lambda|cond|define|type-case|let|letrec|let!|\#lang|require|test|else|first|rest|define-type|define-type-alias|define-struct|not|local|error|lang|module|module*|module+|require|provide|quote|#%datum|#%expression|#%top|#%variable-reference|#%app|lambda|case-lambda|let|let*|letrec|let-values|let*-values|let-syntax|letrec-syntax|let-syntaxes|letrec-syntaxes|letrec-syntaxes+values|local|shared|if|cond|and|or|case|define|else|=>|define|define-values|define-syntax|define-syntaxes|define-for-syntax|define-require-syntax|define-provide-syntax|define-syntax-rule|define-record-type|begin|begin0|begin-for-syntax|when|unless|set!|set!-values|for|for/list|for/vector|for/hash|for/hasheq|for/hasheqv|for/and|for/or|for/lists|for/first|for/last|for/fold|for*|for*/list|for*/vector|for*/hash|for*/hasheq|for*/hasheqv|for*/and|for*/or|for*/lists|for*/first|for*/last|for*/fold|for/fold/derived|for*/fold/derived|define-sequence-syntax|:do-in|do|with-continuation-mark|quasiquote|unquote|unquote-splicing|quote-syntax|#%top-interaction|define-package|open-package|package-begin|define*|define*-values|define*-syntax|define*-syntaxes|open*-package|package?|package-exported-identifiers|package-original-identifiers|block|#%stratified-body|match|match*|match/values|define/match|match-lambda|match-lambda*|match-lambda**|match-let|match-let*|match-let-values|match-let*-values|match-letrec|match-define|match-define-values|with-handlers|with-handlers*|let/cc|let/ec|%|prompt|control|prompt-at|control-at|reset|shift|reset-at|shift-at|prompt0|reset0|control0|shift0|prompt0-at|reset0-at|control0-at|shift0-at|set|cupto|write|display|displayln|print|fprintf|printf|eprintf|format|print-pair-curly-braces|print-mpair-curly-braces|print-unreadable|print-graph|print-struct|print-box|print-vector-length|print-hash-table|print-boolean-long-form|print-reader-abbreviations|print-as-expression|print-syntax-width|current-write-relative-directory|port-write-handler|port-display-handler|port-print-handler|global-port-print-handler)[\s\)]</string>
<key>name</key>
<string>meta.keywords.source.racket</string>
</dict>
<dict>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>constant.language.source.racket</string>
</dict>
</dict>
<key>match</key>
<string>[\s\(](true|false|empty|null)[\s\)]</string>
</dict>
<dict>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>constant.language.source.racket</string>
</dict>
</dict>
<key>match</key>
<string>[\s\(\{\[](#t|#f|#true|#false)[\s\)\}\]]</string>
</dict>
<dict>
<key>captures</key>
<dict>
<key>1</key>
<dict>
<key>name</key>
<string>constant.language.source.racket</string>
</dict>
</dict>
<key>match</key>
<string>(#\\[a-zA-Z0-9_\-?\+\.\!\"]+)</string>
</dict>
<dict>
<key>match</key>
<string>\b(0|([1-9][0-9_]*))\b</string>
<key>name</key>
<string>constant.numeric.integer.source.racket</string>
</dict>
<dict>
<key>begin</key>
<string>;</string>
<key>end</key>
<string>$\n</string>
<key>name</key>
<string>comment.line.documentation.source.racket</string>
</dict>
<dict>
<key>begin</key>
<string>#\|</string>
<key>end</key>
<string>\|#</string>
<key>name</key>
<string>comment.block.source.racket</string>
</dict>
</array>
<key>scopeName</key>
<string>source.racket</string>
<key>uuid</key>
<string>afda310a-1b2e-4b97-a0f7-13819a0ee21f</string>
</dict>
</plist>
@iostate
Copy link

iostate commented Dec 9, 2017

Thank you, so much!

@Searge
Copy link

Searge commented May 29, 2018

Thanks a lot!
I've found some package for brackets highlighting, here is example of config.:

{
  "languages": ["lisp", "scheme", "clojure", "clojurescript", "hylang", "racket", "haskell", "python"],
  "palettes":
  {
    "default": ["violet", "mediumslateblue", "turquoise", "lightgreen", "yellow", "orange", "red"],
    "Tomorrow Night": ["#c66", "#de935f", "#ee6", "#b5bd68", "#81a2be", "#b294bb", "#ff69b4", "#8a2be2"],
    "Monokai": ["#AE81FF", "#66D9EF", "#A6E22E", "#FD971F", "#F92672"]
  }
}

rainbow

@joshgrant
Copy link

This is very nice! Thanks

@dawning7670
Copy link

It is very useful to me. Thanks!

@AlexSlayer69
Copy link

{
	"cmd": ["racket", "$file"],
	"selector": "source.racket",
	"file_regex": "^(.*?):(\\d+):(\\d+):(.*)"
}

For Windows changing build system file to this allowed some of the errors to show inline properly

image

@benknoble
Copy link

RE: vim, I use vim full-time and work in Racket pretty often with it. I re-wrote most of Vim in the Racket Guide and maintain several Racket or Racket-adjacent plugins.

Happy to answer questions about Racket in Vim :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment