summaryrefslogtreecommitdiff
path: root/gui/internal/model/highlight.go
blob: 55742efa5542bccf8985ed603ebf22f0ba46f881 (plain) (blame)
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.
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
}