turbo-editors/turbo-corepublic Fork 0
28d59854361aeda8541d853093e732126f3d7bff
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

highlight.go · 114 lines · 3.7 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 20h ago1package syntax
2
3import (
4 "sort"
5 "strings"
6 "unicode/utf8"
7)
8
9// Highlight returns the spans to colour, one slice per line of src.
10//
11// The result always has exactly as many entries as src has lines, so the
12// editor can index it by line number without checking. Text no span covers —
13// whitespace, and anything the scanner could make nothing of — is drawn in the
14// editor's plain text style.
15//
16// A language this package does not colour gives one empty slice per line,
17// rather than nothing, so the caller still indexes it the same way.
18//
19// spans := syntax.Highlight(syntax.LanguageMarkdown, "# Title\n")
20// fmt.Println(spans[0][0].Class) // heading
21func Highlight(language Language, src string) [][]Span {
22 if definition, ok := registry[language]; ok && definition.Highlight != nil {
23 return definition.Highlight(src)
24 }
25 return make([][]Span, NewLineIndex(src).Count())
26}
27
28// LineIndex knows where each line of a source text starts, and how many runes
29// it holds, which is what turns a byte range into editor coordinates.
30//
31// It is exported because a scanner that works in byte offsets — anything built
32// on a tokeniser rather than on LineScanner, such as Go's go/scanner — needs it
33// to turn those offsets into the per-line spans Highlight promises.
34//
35// lines := syntax.NewLineIndex(src)
36// out := make([][]syntax.Span, lines.Count())
37// lines.AppendSpans(out, start, end, syntax.ClassKeyword)
38type LineIndex struct {
39 src string
40 starts []int // byte offset of the first character of each line
41 runeLens []int // number of runes on each line
42}
43
44// NewLineIndex builds the index for a source text.
45func NewLineIndex(src string) *LineIndex {
46 index := &LineIndex{src: src, starts: []int{0}}
47 for offset, r := range src {
48 if r == '\n' {
49 index.starts = append(index.starts, offset+1)
50 }
51 }
52
53 index.runeLens = make([]int, len(index.starts))
54 for line := range index.starts {
55 index.runeLens[line] = utf8.RuneCountInString(index.text(line))
56 }
57 return index
58}
59
60// Count returns the number of lines, always at least one.
61func (x *LineIndex) Count() int { return len(x.starts) }
62
63// text returns the content of a line, without its terminator.
64func (x *LineIndex) text(line int) string {
65 start := x.starts[line]
66 if line+1 < len(x.starts) {
67 return strings.TrimSuffix(x.src[start:x.starts[line+1]-1], "\r")
68 }
69 return x.src[start:]
70}
71
72// AppendSpans records the byte range [start, end) as one span per line it
73// covers, since a span may never straddle a line break.
74//
75// out must already have one entry per line — Count says how many — and is
76// appended to in place.
77func (x *LineIndex) AppendSpans(out [][]Span, start, end int, class Class) {
78 firstLine, firstCol := x.position(start)
79 lastLine, lastCol := x.position(end)
80
81 if firstLine == lastLine {
82 x.add(out, firstLine, firstCol, lastCol, class)
83 return
84 }
85
86 x.add(out, firstLine, firstCol, x.runeLens[firstLine], class)
87 for line := firstLine + 1; line < lastLine; line++ {
88 x.add(out, line, 0, x.runeLens[line], class)
89 }
90 x.add(out, lastLine, 0, lastCol, class)
91}
92
93// add appends one span, dropping the empty ones so that callers never have to
94// check for them.
95func (x *LineIndex) add(out [][]Span, line, start, end int, class Class) {
96 if line < 0 || line >= len(out) || end <= start {
97 return
98 }
99 out[line] = append(out[line], Span{Start: start, End: end, Class: class})
100}
101
102// position converts a byte offset into a line number and a rune column.
103func (x *LineIndex) position(offset int) (line, col int) {
104 if offset <= 0 {
105 return 0, 0
106 }
107 if offset > len(x.src) {
108 offset = len(x.src)
109 }
110
111 // The line is the last one starting at or before the offset.
112 line = sort.SearchInts(x.starts, offset+1) - 1
113 return line, utf8.RuneCountInString(x.src[x.starts[line]:offset])
114}