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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
|
// SPDX-License-Identifier: GPL-3.0-or-later
package model
// SpanKind is what a stretch of a configuration file is, for colouring it.
type SpanKind int
const (
SpanComment SpanKind = iota
SpanString
SpanHead // the symbol a form starts with
SpanAction // a head that does something to a file
SpanParen
)
// Span is one stretch of text to colour, in character offsets - what a
// GtkTextBuffer counts in, not bytes.
type Span struct {
From, To int
Kind SpanKind
}
// actionHeads are the forms that act on a file rather than describe one.
var actionHeads = map[string]bool{
"copy": true, "move": true, "rename": true, "delete": true, "stop": true,
}
// Spans reads a configuration and says what to colour: a ";" outside a
// string comments out the rest of its line; a string runs to its closing
// quote, a backslash escaping the next character; the first symbol after
// "(" is the form's head. Nothing here knows about widgets, so the rules of
// the little language stay testable (his request, 2026-09-16).
func Spans(text string) []Span {
var out []Span
runes := []rune(text)
afterParen := false
for i := 0; i < len(runes); i++ {
switch c := runes[i]; {
case c == ';':
j := i
for j < len(runes) && runes[j] != '\n' {
j++
}
out = append(out, Span{i, j, SpanComment})
i = j
afterParen = false
case c == '"':
j := i + 1
for j < len(runes) {
if runes[j] == '\\' {
j += 2
continue
}
if runes[j] == '"' {
j++
break
}
j++
}
if j > len(runes) {
j = len(runes)
}
out = append(out, Span{i, j, SpanString})
i = j - 1
afterParen = false
case c == '(' || c == ')':
out = append(out, Span{i, i + 1, SpanParen})
afterParen = c == '('
case c == ' ' || c == '\t' || c == '\n' || c == '\r':
// whitespace between "(" and the head is allowed
default:
j := i
for j < len(runes) && !isDelimiter(runes[j]) {
j++
}
if afterParen {
kind := SpanHead
if actionHeads[string(runes[i:j])] {
kind = SpanAction
}
out = append(out, Span{i, j, kind})
}
i = j - 1
afterParen = false
}
}
return out
}
// isDelimiter reports whether a character ends a symbol.
func isDelimiter(r rune) bool {
switch r {
case ' ', '\t', '\n', '\r', '(', ')', '"', ';':
return true
}
return false
}
|