-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdocument.go
More file actions
139 lines (124 loc) · 4 KB
/
Copy pathdocument.go
File metadata and controls
139 lines (124 loc) · 4 KB
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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
package gopher_textmate
import (
"io"
"strings"
"github.com/andersonpem/gopher-textmate/grammar"
"github.com/andersonpem/gopher-textmate/render"
)
// Document is an incrementally re-tokenized buffer, intended for interactive
// editors and REPLs that repaint on every keystroke. It caches each line's
// tokens together with the tokenizer state that produced it; on SetText it only
// re-tokenizes the lines that actually changed, reusing the unchanged prefix
// and stopping as soon as the tokenizer state reconverges with the previous
// result (the unchanged suffix is reused wholesale).
//
// A Document is not safe for concurrent use; confine it to a single goroutine
// (e.g. the UI loop).
type Document struct {
h *Highlighter
g *grammar.Grammar
scope string
lines []docLine
}
type docLine struct {
text string
start *grammar.StateStack
end *grammar.StateStack
tokens []grammar.Token
}
// NewDocument creates an incremental document tokenized with the given grammar
// scope (empty uses the highlighter's default scope).
func (h *Highlighter) NewDocument(scope string) (*Document, error) {
g, err := h.grammarFor(scope)
if err != nil {
return nil, err
}
return &Document{h: h, g: g, scope: scope}, nil
}
// SetText replaces the document contents, re-tokenizing only what changed.
// It returns the indices of the lines whose tokens changed (useful for partial
// repaints); the slice is nil when nothing changed.
func (d *Document) SetText(text string) []int {
newTexts := splitLines(text)
old := d.lines
newLines := make([]docLine, len(newTexts))
var changed []int
var prevEnd *grammar.StateStack
for i, txt := range newTexts {
start := prevEnd
if i < len(old) && old[i].text == txt && old[i].start.Equals(start) {
// Unchanged text and identical start state: reuse cached tokens and
// end state. This also lets the entire unchanged tail cascade-reuse.
newLines[i] = old[i]
prevEnd = old[i].end
continue
}
toks, end := d.g.TokenizeLine(txt, start)
newLines[i] = docLine{text: txt, start: start, end: end, tokens: toks}
prevEnd = end
changed = append(changed, i)
}
d.lines = newLines
return changed
}
// SetLine replaces a single line's text and re-tokenizes from there until the
// state reconverges. Returns the changed line indices. Out-of-range indices are
// ignored.
func (d *Document) SetLine(i int, text string) []int {
if i < 0 || i >= len(d.lines) {
return nil
}
texts := make([]string, len(d.lines))
for k := range d.lines {
texts[k] = d.lines[k].text
}
texts[i] = text
return d.SetText(strings.Join(texts, "\n"))
}
// Len returns the number of lines.
func (d *Document) Len() int { return len(d.lines) }
// Line returns the tokens for line i (nil if out of range). The returned slice
// must not be mutated.
func (d *Document) Line(i int) []grammar.Token {
if i < 0 || i >= len(d.lines) {
return nil
}
return d.lines[i].tokens
}
// LineText returns the source text of line i.
func (d *Document) LineText(i int) string {
if i < 0 || i >= len(d.lines) {
return ""
}
return d.lines[i].text
}
// RenderLine renders line i to an ANSI string using the highlighter's theme and
// color mode.
func (d *Document) RenderLine(i int) string {
if i < 0 || i >= len(d.lines) {
return ""
}
opts := render.Options{Mode: d.h.mode, Background: d.h.background}
return render.RenderLine(d.lines[i].text, d.lines[i].tokens, d.h.theme, opts)
}
// Render renders the whole document to an ANSI string (lines joined by "\n").
func (d *Document) Render() string {
var b strings.Builder
_ = d.RenderTo(&b)
return b.String()
}
// RenderTo writes the whole document's ANSI rendering to w.
func (d *Document) RenderTo(w io.Writer) error {
opts := render.Options{Mode: d.h.mode, Background: d.h.background}
for i := range d.lines {
if _, err := io.WriteString(w, render.RenderLine(d.lines[i].text, d.lines[i].tokens, d.h.theme, opts)); err != nil {
return err
}
if i < len(d.lines)-1 {
if _, err := io.WriteString(w, "\n"); err != nil {
return err
}
}
}
return nil
}