// License: GPLv3 Copyright: 2023, Kovid Goyal, package diff import ( "errors" "fmt" "io" "os" "path/filepath" "strings" "sync" "kitty/tools/utils" "kitty/tools/utils/images" "github.com/alecthomas/chroma/v2" "github.com/alecthomas/chroma/v2/lexers" "github.com/alecthomas/chroma/v2/styles" ) var _ = fmt.Print var _ = os.WriteFile var ErrNoLexer = errors.New("No lexer available for this format") var DefaultStyle = sync.OnceValue(func() *chroma.Style { // Default style generated by python style.py default pygments.styles.default.DefaultStyle // with https://raw.githubusercontent.com/alecthomas/chroma/master/_tools/style.py return styles.Register(chroma.MustNewStyle("default", chroma.StyleEntries{ chroma.TextWhitespace: "#bbbbbb", chroma.Comment: "italic #3D7B7B", chroma.CommentPreproc: "noitalic #9C6500", chroma.Keyword: "bold #008000", chroma.KeywordPseudo: "nobold", chroma.KeywordType: "nobold #B00040", chroma.Operator: "#666666", chroma.OperatorWord: "bold #AA22FF", chroma.NameBuiltin: "#008000", chroma.NameFunction: "#0000FF", chroma.NameClass: "bold #0000FF", chroma.NameNamespace: "bold #0000FF", chroma.NameException: "bold #CB3F38", chroma.NameVariable: "#19177C", chroma.NameConstant: "#880000", chroma.NameLabel: "#767600", chroma.NameEntity: "bold #717171", chroma.NameAttribute: "#687822", chroma.NameTag: "bold #008000", chroma.NameDecorator: "#AA22FF", chroma.LiteralString: "#BA2121", chroma.LiteralStringDoc: "italic", chroma.LiteralStringInterpol: "bold #A45A77", chroma.LiteralStringEscape: "bold #AA5D1F", chroma.LiteralStringRegex: "#A45A77", chroma.LiteralStringSymbol: "#19177C", chroma.LiteralStringOther: "#008000", chroma.LiteralNumber: "#666666", chroma.GenericHeading: "bold #000080", chroma.GenericSubheading: "bold #800080", chroma.GenericDeleted: "#A00000", chroma.GenericInserted: "#008400", chroma.GenericError: "#E40000", chroma.GenericEmph: "italic", chroma.GenericStrong: "bold", chroma.GenericPrompt: "bold #000080", chroma.GenericOutput: "#717171", chroma.GenericTraceback: "#04D", chroma.Error: "border:#FF0000", chroma.Background: " bg:#f8f8f8", })) }) // Clear the background colour. func clear_background(style *chroma.Style) *chroma.Style { builder := style.Builder() bg := builder.Get(chroma.Background) bg.Background = 0 bg.NoInherit = true builder.AddEntry(chroma.Background, bg) style, _ = builder.Build() return style } func ansi_formatter(w io.Writer, style *chroma.Style, it chroma.Iterator) (err error) { const SGR_PREFIX = "\033[" const SGR_SUFFIX = "m" style = clear_background(style) before, after := make([]byte, 0, 64), make([]byte, 0, 64) nl := []byte{'\n'} write_sgr := func(which []byte) (err error) { if len(which) > 1 { if _, err = w.Write(utils.UnsafeStringToBytes(SGR_PREFIX)); err != nil { return err } if _, err = w.Write(which[:len(which)-1]); err != nil { return err } if _, err = w.Write(utils.UnsafeStringToBytes(SGR_SUFFIX)); err != nil { return err } } return } write := func(text string) (err error) { if err = write_sgr(before); err != nil { return err } if _, err = w.Write(utils.UnsafeStringToBytes(text)); err != nil { return err } if err = write_sgr(after); err != nil { return err } return } for token := it(); token != chroma.EOF; token = it() { entry := style.Get(token.Type) before, after = before[:0], after[:0] if !entry.IsZero() { if entry.Bold == chroma.Yes { before = append(before, '1', ';') after = append(after, '2', '2', '1', ';') } if entry.Underline == chroma.Yes { before = append(before, '4', ';') after = append(after, '2', '4', ';') } if entry.Italic == chroma.Yes { before = append(before, '3', ';') after = append(after, '2', '3', ';') } if entry.Colour.IsSet() { before = append(before, fmt.Sprintf("38:2:%d:%d:%d;", entry.Colour.Red(), entry.Colour.Green(), entry.Colour.Blue())...) after = append(after, '3', '9', ';') } } // independently format each line in a multiline token, needed for the diff kitten highlighting to work, also // pagers like less reset SGR formatting at line boundaries text := sanitize(token.Value) for text != "" { idx := strings.IndexByte(text, '\n') if idx < 0 { if err = write(text); err != nil { return err } break } if err = write(text[:idx]); err != nil { return err } if _, err = w.Write(nl); err != nil { return err } text = text[idx+1:] } } return nil } func resolved_chroma_style(use_light_colors bool) *chroma.Style { name := utils.IfElse(use_light_colors, conf.Pygments_style, conf.Dark_pygments_style) var style *chroma.Style if name == "default" { style = DefaultStyle() } else { style = styles.Get(name) } if style == nil { if resolved_colors.Background.IsDark() && !resolved_colors.Foreground.IsDark() { style = styles.Get("monokai") if style == nil { style = styles.Get("github-dark") } } else { style = DefaultStyle() } if style == nil { style = styles.Fallback } } return style } var tokens_map map[string][]chroma.Token var mu sync.Mutex func highlight_file(path string, use_light_colors bool) (highlighted string, err error) { defer func() { if r := recover(); r != nil { e, ok := r.(error) if !ok { e = fmt.Errorf("%v", r) } err = e } }() filename_for_detection := filepath.Base(path) ext := filepath.Ext(filename_for_detection) if ext != "" { ext = strings.ToLower(ext[1:]) r := conf.Syntax_aliases[ext] if r != "" { filename_for_detection = "file." + r } } text, err := data_for_path(path) if err != nil { return "", err } mu.Lock() if tokens_map == nil { tokens_map = make(map[string][]chroma.Token) } tokens := tokens_map[path] mu.Unlock() if tokens == nil { lexer := lexers.Match(filename_for_detection) if lexer == nil { lexer = lexers.Analyse(text) } if lexer == nil { return "", fmt.Errorf("Cannot highlight %#v: %w", path, ErrNoLexer) } lexer = chroma.Coalesce(lexer) iterator, err := lexer.Tokenise(nil, text) if err != nil { return "", err } tokens = iterator.Tokens() mu.Lock() tokens_map[path] = tokens mu.Unlock() } formatter := chroma.FormatterFunc(ansi_formatter) w := strings.Builder{} w.Grow(len(text) * 2) err = formatter.Format(&w, resolved_chroma_style(use_light_colors), chroma.Literator(tokens...)) // os.WriteFile(filepath.Base(path+".highlighted"), []byte(w.String()), 0o600) return w.String(), err } func highlight_all(paths []string, light bool) { ctx := images.Context{} ctx.Parallel(0, len(paths), func(nums <-chan int) { for i := range nums { path := paths[i] raw, err := highlight_file(path, light) if err != nil { continue } if light { light_highlighted_lines_cache.Set(path, text_to_lines(raw)) } else { dark_highlighted_lines_cache.Set(path, text_to_lines(raw)) } } }) }