4.7 Linemaps
| (require resyntax/grimoire/linemap) | package: resyntax |
A linemap is a precomputed index of a string’s line structure that supports converting between character positions and line numbers. Source code is frequently viewed from one of two perspectives:
A human perspective, either reader or writer, who looks at code as a 2D grid composed of lines and columns.
A machine perspective, that looks at code as one linear sequence of characters (or perhaps even just plain bytes).
Tools that serve as a human-machine interface for code often have to juggle these two perspectives. Linemaps are Resyntax’s tool for doing so. They are used in various places where human concerns related to viewing and describing source code and source edits come up, such as displaying or consuming line-based diffs in the command-line interface. See The Resyntax Command-Line Interface for further details on that matter.
Positions in a linemap are zero-based, but line numbers are one-based. Positions
are character indices into the string. This follows the same convention as Racket’s string operations
such as string-ref, as well as Resyntax’s conventions for string replacements. Line
numbers, however, follow the conventions outlined in
Counting Positions, Lines, and Columns: source files begin at line
1. This matches syntax-line and the conventions of code editors —
Beware that syntax-position and file port positions are one-based, unlike linemap positions. The syntax-line-range operation performs that conversion itself, but positions obtained from syntax objects by other means must be converted before use with a linemap.
The lines of a string are the segments separated by newline characters. The terminating newline is
not part of a line’s contents, but positions of newline characters belong to the lines they
terminate. A string that ends with a newline has a final empty line after it, and the empty string
consists of a single empty line. Note that there are multiple distinct byte sequences that Racket
treats as a newline when reading source code —
procedure
(string-linemap str) → linemap?
str : string?
(define lines (string-linemap "hello\nworld\n"))
> (linemap-position-to-line lines 0) 1
; The trailing newline creates an empty third line. > (linemap-position-to-line lines 12) 3
procedure
(linemap-position-to-line map position)
→ exact-positive-integer? map : linemap? position : exact-nonnegative-integer?
(define lines (string-linemap "hello\nworld\n"))
; Position 5 is line 1's terminating newline, so it belongs to line 1. > (linemap-position-to-line lines 5) 1
> (linemap-position-to-line lines 6) 2
; Positions past the end of the string are out of bounds. > (linemap-position-to-line lines 100) linemap-position-to-line: position is past the end of the
string
position: 100
string length: 12
procedure
(linemap-position-to-start-of-line map position) → exact-nonnegative-integer? map : linemap? position : exact-nonnegative-integer?
(define lines (string-linemap "hello\nworld\n"))
> (linemap-position-to-start-of-line lines 8) 6
; The final empty line starts at the very end of the string. > (linemap-position-to-start-of-line lines 12) 12
procedure
(linemap-position-to-end-of-line map position) → exact-nonnegative-integer? map : linemap? position : exact-nonnegative-integer?
(define lines (string-linemap "hello\nworld\n"))
; Line 1's contents end just before its newline at position 5. > (linemap-position-to-end-of-line lines 2) 5
> (linemap-position-to-end-of-line lines 8) 11
procedure
(syntax-line-range stx #:linemap map) → range?
stx : syntax? map : linemap?
(define src "(define (f x)\n (* x 2))\n") (define in (open-input-string src)) (port-count-lines! in) (define stx (read-syntax 'example in))
> (syntax-line-range stx #:linemap (string-linemap src)) #<range:natural<=> [1, 2]>
; Pairing a syntax object with the wrong linemap is caught (sometimes). > (syntax-line-range stx #:linemap (string-linemap "some other string")) syntax-line-range: syntax object's source location is out of
bounds for the linemap
syntax: #<syntax:example:1:0 (define (f x) (* x 2))>
syntax position: 1
syntax span: 24
string length: 17