aboutsummaryrefslogtreecommitdiff
path: root/gui/internal/model/highlight.go
diff options
context:
space:
mode:
Diffstat (limited to 'gui/internal/model/highlight.go')
-rw-r--r--gui/internal/model/highlight.go97
1 files changed, 97 insertions, 0 deletions
diff --git a/gui/internal/model/highlight.go b/gui/internal/model/highlight.go
new file mode 100644
index 0000000..e34d9d9
--- /dev/null
+++ b/gui/internal/model/highlight.go
@@ -0,0 +1,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
+}