|
3 | 3 | package diff |
4 | 4 |
|
5 | 5 | import ( |
6 | | - "errors" |
7 | 6 | "fmt" |
8 | | - "io" |
9 | 7 | "os" |
10 | | - "path/filepath" |
11 | | - "strings" |
12 | 8 | "sync" |
13 | 9 |
|
| 10 | + "github.com/kovidgoyal/kitty/tools/highlight" |
14 | 11 | "github.com/kovidgoyal/kitty/tools/utils" |
15 | 12 | "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" |
20 | 13 | ) |
21 | 14 |
|
22 | 15 | var _ = fmt.Print |
23 | 16 | var _ = os.WriteFile |
24 | 17 |
|
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 | | - |
161 | 18 | type prefer_light_colors bool |
162 | 19 |
|
163 | 20 | func (s prefer_light_colors) StyleName() string { |
164 | 21 | return utils.IfElse(bool(s), conf.Pygments_style, conf.Dark_pygments_style) |
165 | 22 | } |
166 | 23 |
|
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) } |
200 | 27 |
|
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 | +}) |
255 | 31 |
|
256 | 32 | func highlight_all(paths []string, light bool) { |
257 | 33 | ctx := images.Context{} |
258 | 34 | srd := prefer_light_colors(light) |
259 | 35 | ctx.Parallel(0, len(paths), func(nums <-chan int) { |
260 | 36 | for i := range nums { |
261 | 37 | path := paths[i] |
262 | | - raw, err := HighlightFile(path, &srd) |
| 38 | + raw, err := highlighter().HighlightFile(path, &srd) |
263 | 39 | if err != nil { |
264 | 40 | continue |
265 | 41 | } |
|
0 commit comments