Now we’ll write an indenter, a function that’s used to automatically indent source code in DrRacket. Like syntax coloring, writing an indenter is optional.
When we made our get-info function for jsonic, we included a hook for the indenter with the key drracket:indentation. At the time, we hypothesized a new "jsonic/indenter.rkt" module that would provide an indent-jsonic function. We’ll start that module now, importing br/indent for some helper functions we’ll need shortly, and racket/contract too:
1 2 3 4 5 6 7 8 | #lang br (require br/indent racket/contract) (define (indent-jsonic tbox [posn 0]) ···) (provide (contract-out [indent-jsonic ···])) |
How does our indenting function work? When DrRacket needs to indent some code, it will call our function with a tbox and a posn:
The tbox argument—short for text box—is a Racket user-interface object that contains the source code. Specifically, it’s an instance of the text% class from racket/gui. + Using racket/gui, cross-platform GUI applications can be made entirely within Racket. DrRacket is one example. For now, we won’t detour into racket/gui. But it’s easier to work with the text box—rather than a port or string—because the text box can tell us useful things about the layout of the source code.
The posn argument is a character position inside the text box, indicating the line we need to indent. This value is greater than or equal to zero, so we can test it with exact-nonnegative-integer?. We declare the argument as [posn 0] to indicate that it should have a default value of 0 if only one argument (the text box) is used.
DrRacket expects our indenter to return either an exact-nonnegative-integer? with the total number of spaces to indent the line, or #f if DrRacket should apply its usual S-expression indentation rules. In this case, we’ll calculate the indent for every line, and never return #f. So our contract for that value is simply exact-nonnegative-integer?.
With this information, we can write a contract for our indenter. We import racket/contract to get our contract combinators. Because tbox is an instance of text%, we can test it with the contract combinator is-a?/c, if we also import text% from racket/gui/base. The other predicates for the contract follow from the descriptions above:
1 2 3 |
The twist, however, is that our contract has one mandatory argument, and one optional argument. So instead of the -> contract combinator, we’ll use the ->* variant, which supports optional arguments. We just need to move our mandatory and optional arguments into their own parenthesized groups, like so:
1 2 3 |
Then we can move the finished contract into our provide expression using contract-out:
1 2 3 4 5 6 7 8 9 10 | #lang br (require br/indent racket/contract racket/gui/base ) (define (indent-jsonic tbox [posn 0]) ···) (provide (contract-out [indent-jsonic (((is-a?/c text%)) (exact-nonnegative-integer?) . ->* . exact-nonnegative-integer?)])) |
Our indenter will have four rules:
The indent of the first line is 0.
If the previous line begins with a left bracket—either { or [—then the current line will be indented (= moved to the right) relative to the previous line.
If the current line begins with a right bracket—either } or ]—then the current line will be unindented (= moved to the left) relative to the previous line.
Otherwise, the current line is indented the same distance as the previous line.
So if we start with a plain chunk of source like this:
1 2 3 4 5 6 7 8 9 10 11 12 | #lang jsonic { "value": 42, "string": [ { "array": @$(range 5)$@, "object": @$(hash 'k1 "valstring")$@ } ] // "bar" } |
It will be indented like so:
1 2 3 4 5 6 7 8 9 10 11 12 | #lang jsonic { "value": 42, "string": [ { "array": @$(range 5)$@, "object": @$(hash 'k1 "valstring")$@ } ] // "bar" } |
First let’s establish an indent-width of 2, and set up left-bracket? and right-bracket? as predicates to use within the body of the main function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #lang br (require br/indent racket/contract racket/gui/base) (define indent-width 2) (define (left-bracket? c) (member c (list #\{ #\[))) (define (right-bracket? c) (member c (list #\} #\]))) (define (indent-jsonic tbox [posn 0]) ···) (provide (contract-out [indent-jsonic (((is-a?/c text%)) (exact-nonnegative-integer?) . ->* . exact-nonnegative-integer?)])) |
Our indenter is going to operate on a new data type: the character. A Racket character is a numerical type representing a Unicode value. Characters are the raw ingredients of any string. A character is written with a #\ prefix. Characters can be faster than the equivalent one-character strings, so certain Racket functions use characters. For our predicates, we use member to check whether a character c appears in a list of possibilities. + Those who have studied Racket’s equality functions might know that characters can be compared with eqv? rather than the slower equal?. So in this case, we could use memv, which relies on eqv?, rather than member, which uses equal?.
Moving to the inside of indent-jsonic. We can see from the rules that the information we need to indent a line comes from the current line and the previous line. So we start our indenter by getting those line numbers with the helper functions previous-line and line. (These helper functions are all imported from br/indent.)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #lang br (require br/indent racket/contract racket/gui/base) (define indent-width 2) (define (left-bracket? c) (member c (list #\{ #\[))) (define (right-bracket? c) (member c (list #\} #\]))) (define (indent-jsonic tbox [posn 0]) (define prev-line (previous-line tbox posn)) (define current-line (line tbox posn)) ···) (provide (contract-out [indent-jsonic (((is-a?/c text%)) (exact-nonnegative-integer?) . ->* . exact-nonnegative-integer?)])) |
To finish our function, we compute the indent for the current line. Let’s insert the code, then step through each part:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | #lang br (require br/indent racket/contract racket/gui/base) (define indent-width 2) (define (left-bracket? c) (member c (list #\{ #\[))) (define (right-bracket? c) (member c (list #\} #\]))) (define (indent-jsonic tbox [posn 0]) (define prev-line (previous-line tbox posn)) (define current-line (line tbox posn)) (cond [(not prev-line) 0] [else (define prev-indent (or (line-indent tbox prev-line) 0)) (cond [(left-bracket? (line-first-visible-char tbox prev-line)) (+ prev-indent indent-width)] [(right-bracket? (line-first-visible-char tbox current-line)) (- prev-indent indent-width)] [else prev-indent])])) (provide (contract-out [indent-jsonic (((is-a?/c text%)) (exact-nonnegative-integer?) . ->* . exact-nonnegative-integer?)])) |
Within the branches of our cond expression, we implement our indenting rules:
The first branch handles indenting rule #1. It checks whether prev-line is #false. If so, we must be on the first line, so the indent is 0.
Under the else branch, we insert another cond expression. Since this branch is only reached when we have a prev-line, we can get the prev-indent value.
The first branch of the nested cond handles indenting rule #2. It checks whether the first visible character of the previous line is a left-bracket?. (“Visible” meaning something other than whitespace.) If so, it indents the line by adding indent-width to the prev-indent.
The next branch handles indenting rule #3. It checks whether the first visible character of the current line is a right-bracket?. If so, it unindents the line by subtracting indent-width from the prev-indent.
Otherwise we reach the else branch, which handles indenting rule #4 by returning prev-indent.
As usual, we want to make sure our function works before we move on. As we’ve done before, we’ll set up a test submodule to hold our unit tests. (We’ll only make one. More would be wiser.) We’ll use our sample case as the basis of the test:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | #lang br (require br/indent racket/contract racket/gui/base) (define indent-width 2) (define (left-bracket? c) (member c (list #\{ #\[))) (define (right-bracket? c) (member c (list #\} #\]))) (define (indent-jsonic tbox [posn 0]) (define prev-line (previous-line tbox posn)) (define current-line (line tbox posn)) (cond [(not prev-line) 0] [else (define prev-indent (or (line-indent tbox prev-line) 0)) (cond [(left-bracket? (line-first-visible-char tbox prev-line)) (+ prev-indent indent-width)] [(right-bracket? (line-first-visible-char tbox current-line)) (- prev-indent indent-width)] [else prev-indent])])) (provide (contract-out [indent-jsonic (((is-a?/c text%)) (exact-nonnegative-integer?) . ->* . exact-nonnegative-integer?)])) (module+ test (require rackunit) (define test-str #<<HERE #lang jsonic { "value": 42, "string": [ { "array": @$(range 5)$@, "object": @$(hash 'k1 "valstring")$@ } ] // "bar" } HERE ) (apply-indenter indent-jsonic test-str)) |
Once again, we use the #<<HERE ··· HERE delimiters to create a here string, a convenience for using a string as a literal value without having to escape the double-quote marks within. We can paste whatever we want between the here-string delimiters and it will be assigned to test-str as a value.
In the last line, we use the testing helper apply-indenter, which takes as arguments our indent-jsonic function and our test-str. If we now run "jsonic/indenter.rkt", our test will run and we’ll see the result:
1 | "#lang jsonic\n{\n \"value\",\n \"string\":\n [\n {\n \"array\": @$(range 5)$@,\n \"object\": @$(hash 'k1 \"valstring\")$@\n }\n ]\n // \"bar\"\n}" |
It’s a little hard to see what’s going on. If we change our test to display the result:
1 | (display (apply-indenter indent-jsonic test-str)) |
The indenting will become obvious:
1 2 3 4 5 6 7 8 9 10 11 12 | #lang jsonic { "value": 42, "string": [ { "array": @$(range 5)$@, "object": @$(hash 'k1 "valstring")$@ } ] // "bar" } |
If we compare this to our intended result, we can confirm that our indenter does what we hoped. But if this is going to become a unit test, we need to find a way to quantify the indentation of this string so we can verify it automatically with check-equal?, not with our eyes.
Here’s how we do that:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | #lang br (require br/indent racket/contract racket/gui/base) (define indent-width 2) (define (left-bracket? c) (member c (list #\{ #\[))) (define (right-bracket? c) (member c (list #\} #\]))) (define (indent-jsonic tbox [posn 0]) (define prev-line (previous-line tbox posn)) (define current-line (line tbox posn)) (cond [(not prev-line) 0] [else (define prev-indent (line-indent tbox prev-line)) (cond [(left-bracket? (line-first-visible-char tbox prev-line)) (+ prev-indent indent-width)] [(right-bracket? (line-first-visible-char tbox current-line)) (- prev-indent indent-width)] [else prev-indent])])) (provide (contract-out [indent-jsonic (((is-a?/c text%)) (exact-nonnegative-integer?) . ->* . exact-nonnegative-integer?)])) (module+ test (require rackunit) (define test-str #<<HERE #lang jsonic { "value": 42, "string": [ { "array": @$(range 5)$@, "object": @$(hash 'k1 "valstring")$@ } ] // "bar" } HERE ) (check-equal? (string-indents (apply-indenter indent-jsonic test-str)) '(0 0 2 2 2 4 6 6 4 2 2 0))) |
Instead of display, we’ll pass the result string to string-indents (another helper function from the helpful br/indent) which will list the number of spaces at the beginning of each line. We can easily compare this to our expected list of integers representing indents.
By the way, where did we get this list of indents? One way is to use string-indents on the REPL with our indented string, and then paste the resulting list of values into our unit test.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | (string-indents #<<HERE #lang jsonic { "value": 42, "string": [ { "array": @$(range 5)$@, "object": @$(hash 'k1 "valstring")$@ } ] // "bar" } HERE ) |
1 | '(0 0 2 2 2 4 6 6 4 2 2 0) |
Again, in the real world we’d want to write more unit tests with a wider variety of input strings. But for now, we’ll say that indent-jsonic is complete.
To use our indenter in DrRacket, we have to connect it to our get-info function in "main.rkt". We do that by uncommenting the branch we previously commented out:
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)) |
Then, as we did in syntax coloring, we have to force a refresh of get-info. If we’re using Racket v6.9 or later, we select Racket → Reload #lang Extensions, which reloads our get-info function and our new colorer. If not, we quit and restart DrRacket, which has the same effect. Then we can apply indenting to a jsonic source file by typing ctrl-i (Linux & Windows) or cmd-i (Mac OS).
In this example, indenting is a cosmetic convenience. Everything we’ve done here presumes that whitespace is not semantically meaningful, and thus we can freely indent lines without changing the program result. This is true for jsonic and many other languages.
But not always. Python, for instance, depends on white space to describe the structure of the program. In those cases, an indenter would need to be less casual and more conservative, to ensure that the act of indenting doesn’t change the meaning of the program.