highlight.go 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. // License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
  2. package diff
  3. import (
  4. "errors"
  5. "fmt"
  6. "io"
  7. "os"
  8. "path/filepath"
  9. "strings"
  10. "sync"
  11. "kitty/tools/utils"
  12. "kitty/tools/utils/images"
  13. "github.com/alecthomas/chroma/v2"
  14. "github.com/alecthomas/chroma/v2/lexers"
  15. "github.com/alecthomas/chroma/v2/styles"
  16. )
  17. var _ = fmt.Print
  18. var _ = os.WriteFile
  19. var ErrNoLexer = errors.New("No lexer available for this format")
  20. var DefaultStyle = sync.OnceValue(func() *chroma.Style {
  21. // Default style generated by python style.py default pygments.styles.default.DefaultStyle
  22. // with https://raw.githubusercontent.com/alecthomas/chroma/master/_tools/style.py
  23. return styles.Register(chroma.MustNewStyle("default", chroma.StyleEntries{
  24. chroma.TextWhitespace: "#bbbbbb",
  25. chroma.Comment: "italic #3D7B7B",
  26. chroma.CommentPreproc: "noitalic #9C6500",
  27. chroma.Keyword: "bold #008000",
  28. chroma.KeywordPseudo: "nobold",
  29. chroma.KeywordType: "nobold #B00040",
  30. chroma.Operator: "#666666",
  31. chroma.OperatorWord: "bold #AA22FF",
  32. chroma.NameBuiltin: "#008000",
  33. chroma.NameFunction: "#0000FF",
  34. chroma.NameClass: "bold #0000FF",
  35. chroma.NameNamespace: "bold #0000FF",
  36. chroma.NameException: "bold #CB3F38",
  37. chroma.NameVariable: "#19177C",
  38. chroma.NameConstant: "#880000",
  39. chroma.NameLabel: "#767600",
  40. chroma.NameEntity: "bold #717171",
  41. chroma.NameAttribute: "#687822",
  42. chroma.NameTag: "bold #008000",
  43. chroma.NameDecorator: "#AA22FF",
  44. chroma.LiteralString: "#BA2121",
  45. chroma.LiteralStringDoc: "italic",
  46. chroma.LiteralStringInterpol: "bold #A45A77",
  47. chroma.LiteralStringEscape: "bold #AA5D1F",
  48. chroma.LiteralStringRegex: "#A45A77",
  49. chroma.LiteralStringSymbol: "#19177C",
  50. chroma.LiteralStringOther: "#008000",
  51. chroma.LiteralNumber: "#666666",
  52. chroma.GenericHeading: "bold #000080",
  53. chroma.GenericSubheading: "bold #800080",
  54. chroma.GenericDeleted: "#A00000",
  55. chroma.GenericInserted: "#008400",
  56. chroma.GenericError: "#E40000",
  57. chroma.GenericEmph: "italic",
  58. chroma.GenericStrong: "bold",
  59. chroma.GenericPrompt: "bold #000080",
  60. chroma.GenericOutput: "#717171",
  61. chroma.GenericTraceback: "#04D",
  62. chroma.Error: "border:#FF0000",
  63. chroma.Background: " bg:#f8f8f8",
  64. }))
  65. })
  66. // Clear the background colour.
  67. func clear_background(style *chroma.Style) *chroma.Style {
  68. builder := style.Builder()
  69. bg := builder.Get(chroma.Background)
  70. bg.Background = 0
  71. bg.NoInherit = true
  72. builder.AddEntry(chroma.Background, bg)
  73. style, _ = builder.Build()
  74. return style
  75. }
  76. func ansi_formatter(w io.Writer, style *chroma.Style, it chroma.Iterator) (err error) {
  77. const SGR_PREFIX = "\033["
  78. const SGR_SUFFIX = "m"
  79. style = clear_background(style)
  80. before, after := make([]byte, 0, 64), make([]byte, 0, 64)
  81. nl := []byte{'\n'}
  82. write_sgr := func(which []byte) (err error) {
  83. if len(which) > 1 {
  84. if _, err = w.Write(utils.UnsafeStringToBytes(SGR_PREFIX)); err != nil {
  85. return err
  86. }
  87. if _, err = w.Write(which[:len(which)-1]); err != nil {
  88. return err
  89. }
  90. if _, err = w.Write(utils.UnsafeStringToBytes(SGR_SUFFIX)); err != nil {
  91. return err
  92. }
  93. }
  94. return
  95. }
  96. write := func(text string) (err error) {
  97. if err = write_sgr(before); err != nil {
  98. return err
  99. }
  100. if _, err = w.Write(utils.UnsafeStringToBytes(text)); err != nil {
  101. return err
  102. }
  103. if err = write_sgr(after); err != nil {
  104. return err
  105. }
  106. return
  107. }
  108. for token := it(); token != chroma.EOF; token = it() {
  109. entry := style.Get(token.Type)
  110. before, after = before[:0], after[:0]
  111. if !entry.IsZero() {
  112. if entry.Bold == chroma.Yes {
  113. before = append(before, '1', ';')
  114. after = append(after, '2', '2', '1', ';')
  115. }
  116. if entry.Underline == chroma.Yes {
  117. before = append(before, '4', ';')
  118. after = append(after, '2', '4', ';')
  119. }
  120. if entry.Italic == chroma.Yes {
  121. before = append(before, '3', ';')
  122. after = append(after, '2', '3', ';')
  123. }
  124. if entry.Colour.IsSet() {
  125. before = append(before, fmt.Sprintf("38:2:%d:%d:%d;", entry.Colour.Red(), entry.Colour.Green(), entry.Colour.Blue())...)
  126. after = append(after, '3', '9', ';')
  127. }
  128. }
  129. // independently format each line in a multiline token, needed for the diff kitten highlighting to work, also
  130. // pagers like less reset SGR formatting at line boundaries
  131. text := sanitize(token.Value)
  132. for text != "" {
  133. idx := strings.IndexByte(text, '\n')
  134. if idx < 0 {
  135. if err = write(text); err != nil {
  136. return err
  137. }
  138. break
  139. }
  140. if err = write(text[:idx]); err != nil {
  141. return err
  142. }
  143. if _, err = w.Write(nl); err != nil {
  144. return err
  145. }
  146. text = text[idx+1:]
  147. }
  148. }
  149. return nil
  150. }
  151. func highlight_file(path string) (highlighted string, err error) {
  152. filename_for_detection := filepath.Base(path)
  153. ext := filepath.Ext(filename_for_detection)
  154. if ext != "" {
  155. ext = strings.ToLower(ext[1:])
  156. r := conf.Syntax_aliases[ext]
  157. if r != "" {
  158. filename_for_detection = "file." + r
  159. }
  160. }
  161. text, err := data_for_path(path)
  162. if err != nil {
  163. return "", err
  164. }
  165. lexer := lexers.Match(filename_for_detection)
  166. if lexer == nil {
  167. if err == nil {
  168. lexer = lexers.Analyse(text)
  169. }
  170. }
  171. if lexer == nil {
  172. return "", fmt.Errorf("Cannot highlight %#v: %w", path, ErrNoLexer)
  173. }
  174. lexer = chroma.Coalesce(lexer)
  175. name := conf.Pygments_style
  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 conf.Background.IsDark() && !conf.Foreground.IsDark() {
  184. style = styles.Get("monokai")
  185. if style == nil {
  186. style = styles.Get("github-dark")
  187. }
  188. } else {
  189. style = DefaultStyle()
  190. }
  191. if style == nil {
  192. style = styles.Fallback
  193. }
  194. }
  195. iterator, err := lexer.Tokenise(nil, text)
  196. if err != nil {
  197. return "", err
  198. }
  199. formatter := chroma.FormatterFunc(ansi_formatter)
  200. w := strings.Builder{}
  201. w.Grow(len(text) * 2)
  202. err = formatter.Format(&w, style, iterator)
  203. // os.WriteFile(filepath.Base(path+".highlighted"), []byte(w.String()), 0o600)
  204. return w.String(), err
  205. }
  206. func highlight_all(paths []string) {
  207. ctx := images.Context{}
  208. ctx.Parallel(0, len(paths), func(nums <-chan int) {
  209. for i := range nums {
  210. path := paths[i]
  211. raw, err := highlight_file(path)
  212. if err == nil {
  213. highlighted_lines_cache.Set(path, text_to_lines(raw))
  214. }
  215. }
  216. })
  217. }