On this page:
linemap?
string-linemap
linemap-position-to-line
linemap-position-to-start-of-line
linemap-position-to-end-of-line
syntax-line-range
9.2

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 — line numbers are almost exclusively useful in user interfaces, where one-based numbering is expected.

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 — for further details on how Resyntax handles these cases, see the notes in with-input-from-source.

procedure

(linemap? v)  boolean?

  v : any/c
A predicate that recognizes linemaps.

procedure

(string-linemap str)  linemap?

  str : string?
Constructs a linemap of the lines in str. Only #\newline characters are treated as line separators. In particular, Windows-style "\r\n" line endings are not understood. This never arises in practice, because Resyntax normalizes all newlines to #\newline when reading source code see with-input-from-source for details on that normalization and why it matters.

Examples:
(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?
Returns the line number of the line containing position. The position of a newline character is considered contained by the line that the newline terminates. The position equal to the length of the string — the string’s exclusive end position — is allowed, and belongs to the last line. Positions greater than that raise a contract error.

Examples:
(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?
Returns the position of the first character of the line containing position. If the string ends with a newline and position is on the final, empty line after it, that line’s start position is equal to the length of the string. Like linemap-position-to-line, positions greater than the length of the string raise a contract error.

Examples:
(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?
Returns the position just past the last character of the contents of the line containing position that is, the position of the line’s terminating newline, or the length of the string if the line is the last one. Like linemap-position-to-line, positions greater than the length of the string raise a contract error.

Examples:
(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?
Returns a closed range (with natural<=> as its comparator) containing the line numbers of every line that stx spans, from the line on which it begins to the line on which it ends. The source location of stx must refer to positions within the string that map was built from. If the source location extends past the end of that string, a contract error is raised. This is only partial protection against accidentally pairing a syntax object with a linemap built from some other, unrelated string, but it’s better than nothing.

Examples:
(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