Skip to content

Commit 28fce00

Browse files
committed
Make highlight code fully re-useable
1 parent 2bdbbd9 commit 28fce00

File tree

6 files changed

+304
-252
lines changed

6 files changed

+304
-252
lines changed

kittens/diff/collect.go

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ import (
99
"os"
1010
"path/filepath"
1111
"strings"
12+
"sync"
1213
"unicode/utf8"
1314

15+
"github.com/kovidgoyal/kitty/tools/highlight"
1416
"github.com/kovidgoyal/kitty/tools/utils"
1517
)
1618

@@ -117,33 +119,24 @@ func hash_for_path(path string) (string, error) {
117119

118120
}
119121

120-
// Remove all control codes except newlines
121-
func sanitize_control_codes(x string) string {
122-
pat := utils.MustCompile("[\x00-\x09\x0b-\x1f\x7f\u0080-\u009f]")
123-
return pat.ReplaceAllLiteralString(x, "░")
124-
}
125-
126-
func sanitize_tabs_and_carriage_returns(x string) string {
127-
return strings.NewReplacer("\t", conf.Replace_tab_by, "\r", "⏎").Replace(x)
128-
}
129-
130-
func sanitize(x string) string {
131-
return sanitize_control_codes(sanitize_tabs_and_carriage_returns(x))
132-
}
133-
134122
func text_to_lines(text string) []string {
135123
lines := make([]string, 0, 512)
136124
splitlines_like_git(text, false, func(line string) { lines = append(lines, line) })
137125
return lines
138126
}
139127

128+
var sanitize = sync.OnceValue(func() func(string) string {
129+
s := highlight.NewSanitizeControlCodes(conf.Replace_tab_by)
130+
return s.Sanitize
131+
})
132+
140133
func lines_for_path(path string) ([]string, error) {
141134
return lines_cache.GetOrCreate(path, func(path string) ([]string, error) {
142135
ans, err := data_for_path(path)
143136
if err != nil {
144137
return nil, err
145138
}
146-
return text_to_lines(sanitize(ans)), nil
139+
return text_to_lines(sanitize()(ans)), nil
147140
})
148141
}
149142

kittens/diff/highlight.go

Lines changed: 8 additions & 232 deletions
Original file line numberDiff line numberDiff line change
@@ -3,263 +3,39 @@
33
package diff
44

55
import (
6-
"errors"
76
"fmt"
8-
"io"
97
"os"
10-
"path/filepath"
11-
"strings"
128
"sync"
139

10+
"github.com/kovidgoyal/kitty/tools/highlight"
1411
"github.com/kovidgoyal/kitty/tools/utils"
1512
"github.com/kovidgoyal/kitty/tools/utils/images"
16-
17-
"github.com/alecthomas/chroma/v2"
18-
"github.com/alecthomas/chroma/v2/lexers"
19-
"github.com/alecthomas/chroma/v2/styles"
2013
)
2114

2215
var _ = fmt.Print
2316
var _ = os.WriteFile
2417

25-
var ErrNoLexer = errors.New("No lexer available for this format")
26-
var DefaultStyle = sync.OnceValue(func() *chroma.Style {
27-
// Default style generated by python style.py default pygments.styles.default.DefaultStyle
28-
// with https://raw.githubusercontent.com/alecthomas/chroma/master/_tools/style.py
29-
return styles.Register(chroma.MustNewStyle("default", chroma.StyleEntries{
30-
chroma.TextWhitespace: "#bbbbbb",
31-
chroma.Comment: "italic #3D7B7B",
32-
chroma.CommentPreproc: "noitalic #9C6500",
33-
chroma.Keyword: "bold #008000",
34-
chroma.KeywordPseudo: "nobold",
35-
chroma.KeywordType: "nobold #B00040",
36-
chroma.Operator: "#666666",
37-
chroma.OperatorWord: "bold #AA22FF",
38-
chroma.NameBuiltin: "#008000",
39-
chroma.NameFunction: "#0000FF",
40-
chroma.NameClass: "bold #0000FF",
41-
chroma.NameNamespace: "bold #0000FF",
42-
chroma.NameException: "bold #CB3F38",
43-
chroma.NameVariable: "#19177C",
44-
chroma.NameConstant: "#880000",
45-
chroma.NameLabel: "#767600",
46-
chroma.NameEntity: "bold #717171",
47-
chroma.NameAttribute: "#687822",
48-
chroma.NameTag: "bold #008000",
49-
chroma.NameDecorator: "#AA22FF",
50-
chroma.LiteralString: "#BA2121",
51-
chroma.LiteralStringDoc: "italic",
52-
chroma.LiteralStringInterpol: "bold #A45A77",
53-
chroma.LiteralStringEscape: "bold #AA5D1F",
54-
chroma.LiteralStringRegex: "#A45A77",
55-
chroma.LiteralStringSymbol: "#19177C",
56-
chroma.LiteralStringOther: "#008000",
57-
chroma.LiteralNumber: "#666666",
58-
chroma.GenericHeading: "bold #000080",
59-
chroma.GenericSubheading: "bold #800080",
60-
chroma.GenericDeleted: "#A00000",
61-
chroma.GenericInserted: "#008400",
62-
chroma.GenericError: "#E40000",
63-
chroma.GenericEmph: "italic",
64-
chroma.GenericStrong: "bold",
65-
chroma.GenericPrompt: "bold #000080",
66-
chroma.GenericOutput: "#717171",
67-
chroma.GenericTraceback: "#04D",
68-
chroma.Error: "border:#FF0000",
69-
chroma.Background: " bg:#f8f8f8",
70-
}))
71-
})
72-
73-
// Clear the background colour.
74-
func clear_background(style *chroma.Style) *chroma.Style {
75-
builder := style.Builder()
76-
bg := builder.Get(chroma.Background)
77-
bg.Background = 0
78-
bg.NoInherit = true
79-
builder.AddEntry(chroma.Background, bg)
80-
style, _ = builder.Build()
81-
return style
82-
}
83-
84-
func ansi_formatter(w io.Writer, style *chroma.Style, it chroma.Iterator) (err error) {
85-
const SGR_PREFIX = "\033["
86-
const SGR_SUFFIX = "m"
87-
style = clear_background(style)
88-
before, after := make([]byte, 0, 64), make([]byte, 0, 64)
89-
nl := []byte{'\n'}
90-
write_sgr := func(which []byte) (err error) {
91-
if len(which) > 1 {
92-
if _, err = w.Write(utils.UnsafeStringToBytes(SGR_PREFIX)); err != nil {
93-
return err
94-
}
95-
if _, err = w.Write(which[:len(which)-1]); err != nil {
96-
return err
97-
}
98-
if _, err = w.Write(utils.UnsafeStringToBytes(SGR_SUFFIX)); err != nil {
99-
return err
100-
}
101-
}
102-
return
103-
}
104-
write := func(text string) (err error) {
105-
if err = write_sgr(before); err != nil {
106-
return err
107-
}
108-
if _, err = w.Write(utils.UnsafeStringToBytes(text)); err != nil {
109-
return err
110-
}
111-
if err = write_sgr(after); err != nil {
112-
return err
113-
}
114-
return
115-
}
116-
117-
for token := it(); token != chroma.EOF; token = it() {
118-
entry := style.Get(token.Type)
119-
before, after = before[:0], after[:0]
120-
if !entry.IsZero() {
121-
if entry.Bold == chroma.Yes {
122-
before = append(before, '1', ';')
123-
after = append(after, '2', '2', '1', ';')
124-
}
125-
if entry.Underline == chroma.Yes {
126-
before = append(before, '4', ';')
127-
after = append(after, '2', '4', ';')
128-
}
129-
if entry.Italic == chroma.Yes {
130-
before = append(before, '3', ';')
131-
after = append(after, '2', '3', ';')
132-
}
133-
if entry.Colour.IsSet() {
134-
before = append(before, fmt.Sprintf("38:2:%d:%d:%d;", entry.Colour.Red(), entry.Colour.Green(), entry.Colour.Blue())...)
135-
after = append(after, '3', '9', ';')
136-
}
137-
}
138-
// independently format each line in a multiline token, needed for the diff kitten highlighting to work, also
139-
// pagers like less reset SGR formatting at line boundaries
140-
text := sanitize(token.Value)
141-
for text != "" {
142-
idx := strings.IndexByte(text, '\n')
143-
if idx < 0 {
144-
if err = write(text); err != nil {
145-
return err
146-
}
147-
break
148-
}
149-
if err = write(text[:idx]); err != nil {
150-
return err
151-
}
152-
if _, err = w.Write(nl); err != nil {
153-
return err
154-
}
155-
text = text[idx+1:]
156-
}
157-
}
158-
return nil
159-
}
160-
16118
type prefer_light_colors bool
16219

16320
func (s prefer_light_colors) StyleName() string {
16421
return utils.IfElse(bool(s), conf.Pygments_style, conf.Dark_pygments_style)
16522
}
16623

167-
func (s prefer_light_colors) UseLightColors() bool { return bool(s) }
168-
169-
type StyleResolveData interface {
170-
StyleName() string
171-
UseLightColors() bool
172-
}
173-
174-
func resolved_chroma_style(srd StyleResolveData) *chroma.Style {
175-
name := srd.StyleName()
176-
var style *chroma.Style
177-
if name == "default" {
178-
style = DefaultStyle()
179-
} else {
180-
style = styles.Get(name)
181-
}
182-
if style == nil {
183-
if srd.UseLightColors() {
184-
style = DefaultStyle()
185-
} else {
186-
style = styles.Get("monokai")
187-
if style == nil {
188-
style = styles.Get("github-dark")
189-
}
190-
}
191-
if style == nil {
192-
style = styles.Fallback
193-
}
194-
}
195-
return style
196-
}
197-
198-
var tokens_map map[string][]chroma.Token
199-
var mu sync.Mutex
24+
func (s prefer_light_colors) UseLightColors() bool { return bool(s) }
25+
func (s prefer_light_colors) SyntaxAliases() map[string]string { return conf.Syntax_aliases }
26+
func (s prefer_light_colors) TextForPath(path string) (string, error) { return data_for_path(path) }
20027

201-
func HighlightFile(path string, srd StyleResolveData) (highlighted string, err error) {
202-
defer func() {
203-
if r := recover(); r != nil {
204-
e, ok := r.(error)
205-
if !ok {
206-
e = fmt.Errorf("%v", r)
207-
}
208-
err = e
209-
}
210-
}()
211-
filename_for_detection := filepath.Base(path)
212-
ext := filepath.Ext(filename_for_detection)
213-
if ext != "" {
214-
ext = strings.ToLower(ext[1:])
215-
r := conf.Syntax_aliases[ext]
216-
if r != "" {
217-
filename_for_detection = "file." + r
218-
}
219-
}
220-
text, err := data_for_path(path)
221-
if err != nil {
222-
return "", err
223-
}
224-
mu.Lock()
225-
if tokens_map == nil {
226-
tokens_map = make(map[string][]chroma.Token)
227-
}
228-
tokens := tokens_map[path]
229-
mu.Unlock()
230-
if tokens == nil {
231-
lexer := lexers.Match(filename_for_detection)
232-
if lexer == nil {
233-
lexer = lexers.Analyse(text)
234-
}
235-
if lexer == nil {
236-
return "", fmt.Errorf("Cannot highlight %#v: %w", path, ErrNoLexer)
237-
}
238-
lexer = chroma.Coalesce(lexer)
239-
iterator, err := lexer.Tokenise(nil, text)
240-
if err != nil {
241-
return "", err
242-
}
243-
tokens = iterator.Tokens()
244-
mu.Lock()
245-
tokens_map[path] = tokens
246-
mu.Unlock()
247-
}
248-
formatter := chroma.FormatterFunc(ansi_formatter)
249-
w := strings.Builder{}
250-
w.Grow(len(text) * 2)
251-
err = formatter.Format(&w, resolved_chroma_style(srd), chroma.Literator(tokens...))
252-
// os.WriteFile(filepath.Base(path+".highlighted"), []byte(w.String()), 0o600)
253-
return w.String(), err
254-
}
28+
var highlighter = sync.OnceValue(func() highlight.Highlighter {
29+
return highlight.NewHighlighter(sanitize())
30+
})
25531

25632
func highlight_all(paths []string, light bool) {
25733
ctx := images.Context{}
25834
srd := prefer_light_colors(light)
25935
ctx.Parallel(0, len(paths), func(nums <-chan int) {
26036
for i := range nums {
26137
path := paths[i]
262-
raw, err := HighlightFile(path, &srd)
38+
raw, err := highlighter().HighlightFile(path, &srd)
26339
if err != nil {
26440
continue
26541
}

kittens/diff/render.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -284,10 +284,10 @@ func title_lines(left_path, right_path string, columns, margin_size int, ans []*
284284
}
285285
sl := ScreenLine{}
286286
if right_name != "" && right_name != left_name {
287-
sl.left.marked_up_text = format_as_sgr.title + fit_in(sanitize(left_name), available_cols)
288-
sl.right.marked_up_text = format_as_sgr.title + fit_in(sanitize(right_name), available_cols)
287+
sl.left.marked_up_text = format_as_sgr.title + fit_in(sanitize()(left_name), available_cols)
288+
sl.right.marked_up_text = format_as_sgr.title + fit_in(sanitize()(right_name), available_cols)
289289
} else {
290-
sl.left.marked_up_text = format_as_sgr.title + fit_in(sanitize(left_name), columns-margin_size)
290+
sl.left.marked_up_text = format_as_sgr.title + fit_in(sanitize()(left_name), columns-margin_size)
291291
ll.is_full_width = true
292292
}
293293
l2 := ll
@@ -755,7 +755,7 @@ func rename_lines(path, other_path string, columns, margin_size int, ans []*Logi
755755
ll := LogicalLine{
756756
left_reference: Reference{path: path}, right_reference: Reference{path: other_path},
757757
line_type: CHANGE_LINE, is_change_start: true, is_full_width: true}
758-
for _, line := range splitlines(fmt.Sprintf(`The file %s was renamed to %s`, sanitize(path_name_map[path]), sanitize(path_name_map[other_path])), columns-margin_size) {
758+
for _, line := range splitlines(fmt.Sprintf(`The file %s was renamed to %s`, sanitize()(path_name_map[path]), sanitize()(path_name_map[other_path])), columns-margin_size) {
759759
sl := ScreenLine{}
760760
sl.right.marked_up_text = line
761761
ll.screen_lines = append(ll.screen_lines, &sl)

kittens/diff/ui.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -446,7 +446,7 @@ func (self *Handler) draw_status_line() {
446446
if self.inputting_command {
447447
self.rl.RedrawNonAtomic()
448448
} else if self.statusline_message != "" {
449-
self.lp.QueueWriteString(message_format(wcswidth.TruncateToVisualLength(sanitize(self.statusline_message), self.screen_size.columns)))
449+
self.lp.QueueWriteString(message_format(wcswidth.TruncateToVisualLength(sanitize()(self.statusline_message), self.screen_size.columns)))
450450
} else {
451451
num := self.logical_lines.NumScreenLinesTo(self.scroll_pos)
452452
den := self.logical_lines.NumScreenLinesTo(self.max_scroll_pos)

0 commit comments

Comments
 (0)