render.go 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841
  1. // License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
  2. package diff
  3. import (
  4. "errors"
  5. "fmt"
  6. "math"
  7. "os"
  8. "strconv"
  9. "strings"
  10. "kitty/tools/tui/graphics"
  11. "kitty/tools/tui/loop"
  12. "kitty/tools/tui/sgr"
  13. "kitty/tools/utils"
  14. "kitty/tools/utils/style"
  15. "kitty/tools/wcswidth"
  16. )
  17. var _ = fmt.Print
  18. type LineType int
  19. const (
  20. TITLE_LINE LineType = iota
  21. CHANGE_LINE
  22. CONTEXT_LINE
  23. HUNK_TITLE_LINE
  24. IMAGE_LINE
  25. EMPTY_LINE
  26. )
  27. type Reference struct {
  28. path string
  29. linenum int // 1 based
  30. }
  31. type HalfScreenLine struct {
  32. marked_up_margin_text string
  33. marked_up_text string
  34. is_filler bool
  35. cached_wcswidth int
  36. }
  37. func (self *HalfScreenLine) wcswidth() int {
  38. if self.cached_wcswidth == 0 && self.marked_up_text != "" {
  39. self.cached_wcswidth = wcswidth.Stringwidth(self.marked_up_text)
  40. }
  41. return self.cached_wcswidth
  42. }
  43. type ScreenLine struct {
  44. left, right HalfScreenLine
  45. }
  46. type LogicalLine struct {
  47. line_type LineType
  48. screen_lines []*ScreenLine
  49. is_full_width bool
  50. is_change_start bool
  51. left_reference, right_reference Reference
  52. left_image, right_image struct {
  53. key string
  54. count int
  55. }
  56. image_lines_offset int
  57. }
  58. func (self *LogicalLine) render_screen_line(n int, lp *loop.Loop, margin_size, columns int) {
  59. if n >= len(self.screen_lines) || n < 0 {
  60. return
  61. }
  62. sl := self.screen_lines[n]
  63. available_cols := columns/2 - margin_size
  64. if self.is_full_width {
  65. available_cols = columns - margin_size
  66. }
  67. left_margin := place_in(sl.left.marked_up_margin_text, margin_size)
  68. left_text := place_in(sl.left.marked_up_text, available_cols)
  69. if sl.left.is_filler {
  70. left_margin = format_as_sgr.margin_filler + left_margin
  71. left_text = format_as_sgr.filler + left_text
  72. } else {
  73. switch self.line_type {
  74. case CHANGE_LINE, IMAGE_LINE:
  75. left_margin = format_as_sgr.removed_margin + left_margin
  76. left_text = format_as_sgr.removed + left_text
  77. case HUNK_TITLE_LINE:
  78. left_margin = format_as_sgr.hunk_margin + left_margin
  79. left_text = format_as_sgr.hunk + left_text
  80. case TITLE_LINE:
  81. default:
  82. left_margin = format_as_sgr.margin + left_margin
  83. }
  84. }
  85. lp.QueueWriteString(left_margin + "\x1b[m")
  86. lp.QueueWriteString(left_text)
  87. if self.is_full_width {
  88. return
  89. }
  90. right_margin := place_in(sl.right.marked_up_margin_text, margin_size)
  91. right_text := place_in(sl.right.marked_up_text, available_cols)
  92. if sl.right.is_filler {
  93. right_margin = format_as_sgr.margin_filler + right_margin
  94. right_text = format_as_sgr.filler + right_text
  95. } else {
  96. switch self.line_type {
  97. case CHANGE_LINE, IMAGE_LINE:
  98. right_margin = format_as_sgr.added_margin + right_margin
  99. right_text = format_as_sgr.added + right_text
  100. case HUNK_TITLE_LINE:
  101. right_margin = format_as_sgr.hunk_margin + right_margin
  102. right_text = format_as_sgr.hunk + right_text
  103. case TITLE_LINE:
  104. default:
  105. right_margin = format_as_sgr.margin + right_margin
  106. }
  107. }
  108. lp.QueueWriteString("\x1b[m\r")
  109. lp.MoveCursorHorizontally(available_cols + margin_size)
  110. lp.QueueWriteString(right_margin + "\x1b[m")
  111. lp.QueueWriteString(right_text)
  112. }
  113. func (self *LogicalLine) IncrementScrollPosBy(pos *ScrollPos, amt int) (delta int) {
  114. if len(self.screen_lines) > 0 {
  115. npos := utils.Max(0, utils.Min(pos.screen_line+amt, len(self.screen_lines)-1))
  116. delta = npos - pos.screen_line
  117. pos.screen_line = npos
  118. }
  119. return
  120. }
  121. func fit_in(text string, count int) string {
  122. truncated := wcswidth.TruncateToVisualLength(text, count)
  123. if len(truncated) >= len(text) {
  124. return text
  125. }
  126. if count > 1 {
  127. truncated = wcswidth.TruncateToVisualLength(text, count-1)
  128. }
  129. return truncated + `…`
  130. }
  131. func fill_in(text string, sz int) string {
  132. w := wcswidth.Stringwidth(text)
  133. if w < sz {
  134. text += strings.Repeat(` `, (sz - w))
  135. }
  136. return text
  137. }
  138. func place_in(text string, sz int) string {
  139. return fill_in(fit_in(text, sz), sz)
  140. }
  141. var format_as_sgr struct {
  142. title, margin, added, removed, added_margin, removed_margin, filler, margin_filler, hunk_margin, hunk, selection, search string
  143. }
  144. var statusline_format, added_count_format, removed_count_format, message_format func(...any) string
  145. var use_light_colors bool = false
  146. type ResolvedColors struct {
  147. Added_bg style.RGBA
  148. Added_margin_bg style.RGBA
  149. Background style.RGBA
  150. Filler_bg style.RGBA
  151. Foreground style.RGBA
  152. Highlight_added_bg style.RGBA
  153. Highlight_removed_bg style.RGBA
  154. Hunk_bg style.RGBA
  155. Hunk_margin_bg style.RGBA
  156. Margin_bg style.RGBA
  157. Margin_fg style.RGBA
  158. Margin_filler_bg style.NullableColor
  159. Removed_bg style.RGBA
  160. Removed_margin_bg style.RGBA
  161. Search_bg style.RGBA
  162. Search_fg style.RGBA
  163. Select_bg style.RGBA
  164. Select_fg style.NullableColor
  165. Title_bg style.RGBA
  166. Title_fg style.RGBA
  167. }
  168. var resolved_colors ResolvedColors
  169. func create_formatters() {
  170. rc := &resolved_colors
  171. if !use_light_colors {
  172. rc.Added_bg = conf.Dark_added_bg
  173. rc.Added_margin_bg = conf.Dark_added_margin_bg
  174. rc.Background = conf.Dark_background
  175. rc.Filler_bg = conf.Dark_filler_bg
  176. rc.Foreground = conf.Dark_foreground
  177. rc.Highlight_added_bg = conf.Dark_highlight_added_bg
  178. rc.Highlight_removed_bg = conf.Dark_highlight_removed_bg
  179. rc.Hunk_bg = conf.Dark_hunk_bg
  180. rc.Hunk_margin_bg = conf.Dark_hunk_margin_bg
  181. rc.Margin_bg = conf.Dark_margin_bg
  182. rc.Margin_fg = conf.Dark_margin_fg
  183. rc.Margin_filler_bg = conf.Dark_margin_filler_bg
  184. rc.Removed_bg = conf.Dark_removed_bg
  185. rc.Removed_margin_bg = conf.Dark_removed_margin_bg
  186. rc.Search_bg = conf.Dark_search_bg
  187. rc.Search_fg = conf.Dark_search_fg
  188. rc.Select_bg = conf.Dark_select_bg
  189. rc.Select_fg = conf.Dark_select_fg
  190. rc.Title_bg = conf.Dark_title_bg
  191. rc.Title_fg = conf.Dark_title_fg
  192. } else {
  193. rc.Added_bg = conf.Added_bg
  194. rc.Added_margin_bg = conf.Added_margin_bg
  195. rc.Background = conf.Background
  196. rc.Filler_bg = conf.Filler_bg
  197. rc.Foreground = conf.Foreground
  198. rc.Highlight_added_bg = conf.Highlight_added_bg
  199. rc.Highlight_removed_bg = conf.Highlight_removed_bg
  200. rc.Hunk_bg = conf.Hunk_bg
  201. rc.Hunk_margin_bg = conf.Hunk_margin_bg
  202. rc.Margin_bg = conf.Margin_bg
  203. rc.Margin_fg = conf.Margin_fg
  204. rc.Margin_filler_bg = conf.Margin_filler_bg
  205. rc.Removed_bg = conf.Removed_bg
  206. rc.Removed_margin_bg = conf.Removed_margin_bg
  207. rc.Search_bg = conf.Search_bg
  208. rc.Search_fg = conf.Search_fg
  209. rc.Select_bg = conf.Select_bg
  210. rc.Select_fg = conf.Select_fg
  211. rc.Title_bg = conf.Title_bg
  212. rc.Title_fg = conf.Title_fg
  213. }
  214. ctx := style.Context{AllowEscapeCodes: true}
  215. only_open := func(x string) string {
  216. ans := ctx.SprintFunc(x)("|")
  217. ans, _, _ = strings.Cut(ans, "|")
  218. return ans
  219. }
  220. format_as_sgr.filler = only_open("bg=" + rc.Filler_bg.AsRGBSharp())
  221. if rc.Margin_filler_bg.IsSet {
  222. format_as_sgr.margin_filler = only_open("bg=" + rc.Margin_filler_bg.Color.AsRGBSharp())
  223. } else {
  224. format_as_sgr.margin_filler = only_open("bg=" + rc.Filler_bg.AsRGBSharp())
  225. }
  226. format_as_sgr.added = only_open("bg=" + rc.Added_bg.AsRGBSharp())
  227. format_as_sgr.added_margin = only_open(fmt.Sprintf("fg=%s bg=%s", rc.Margin_fg.AsRGBSharp(), rc.Added_margin_bg.AsRGBSharp()))
  228. format_as_sgr.removed = only_open("bg=" + rc.Removed_bg.AsRGBSharp())
  229. format_as_sgr.removed_margin = only_open(fmt.Sprintf("fg=%s bg=%s", rc.Margin_fg.AsRGBSharp(), rc.Removed_margin_bg.AsRGBSharp()))
  230. format_as_sgr.title = only_open(fmt.Sprintf("fg=%s bg=%s bold", rc.Title_fg.AsRGBSharp(), rc.Title_bg.AsRGBSharp()))
  231. format_as_sgr.margin = only_open(fmt.Sprintf("fg=%s bg=%s", rc.Margin_fg.AsRGBSharp(), rc.Margin_bg.AsRGBSharp()))
  232. format_as_sgr.hunk = only_open(fmt.Sprintf("fg=%s bg=%s", rc.Margin_fg.AsRGBSharp(), rc.Hunk_bg.AsRGBSharp()))
  233. format_as_sgr.hunk_margin = only_open(fmt.Sprintf("fg=%s bg=%s", rc.Margin_fg.AsRGBSharp(), rc.Hunk_margin_bg.AsRGBSharp()))
  234. format_as_sgr.search = only_open(fmt.Sprintf("fg=%s bg=%s", rc.Search_fg.AsRGBSharp(), rc.Search_bg.AsRGBSharp()))
  235. statusline_format = ctx.SprintFunc(fmt.Sprintf("fg=%s", rc.Margin_fg.AsRGBSharp()))
  236. added_count_format = ctx.SprintFunc(fmt.Sprintf("fg=%s", rc.Highlight_added_bg.AsRGBSharp()))
  237. removed_count_format = ctx.SprintFunc(fmt.Sprintf("fg=%s", rc.Highlight_removed_bg.AsRGBSharp()))
  238. message_format = ctx.SprintFunc("bold")
  239. if rc.Select_fg.IsSet {
  240. format_as_sgr.selection = only_open(fmt.Sprintf("fg=%s bg=%s", rc.Select_fg.Color.AsRGBSharp(), rc.Select_bg.AsRGBSharp()))
  241. } else {
  242. format_as_sgr.selection = only_open("bg=" + rc.Select_bg.AsRGBSharp())
  243. }
  244. }
  245. func center_span(ltype string, offset, size int) *sgr.Span {
  246. ans := sgr.NewSpan(offset, size)
  247. switch ltype {
  248. case "add":
  249. ans.SetBackground(resolved_colors.Highlight_added_bg).SetClosingBackground(resolved_colors.Added_bg)
  250. case "remove":
  251. ans.SetBackground(resolved_colors.Highlight_removed_bg).SetClosingBackground(resolved_colors.Removed_bg)
  252. }
  253. return ans
  254. }
  255. func title_lines(left_path, right_path string, columns, margin_size int, ans []*LogicalLine) []*LogicalLine {
  256. left_name, right_name := path_name_map[left_path], path_name_map[right_path]
  257. available_cols := columns/2 - margin_size
  258. ll := LogicalLine{
  259. line_type: TITLE_LINE,
  260. left_reference: Reference{path: left_path}, right_reference: Reference{path: right_path},
  261. }
  262. sl := ScreenLine{}
  263. if right_name != "" && right_name != left_name {
  264. sl.left.marked_up_text = format_as_sgr.title + fit_in(sanitize(left_name), available_cols)
  265. sl.right.marked_up_text = format_as_sgr.title + fit_in(sanitize(right_name), available_cols)
  266. } else {
  267. sl.left.marked_up_text = format_as_sgr.title + fit_in(sanitize(left_name), columns-margin_size)
  268. ll.is_full_width = true
  269. }
  270. l2 := ll
  271. l2.line_type = EMPTY_LINE
  272. ll.screen_lines = append(ll.screen_lines, &sl)
  273. sl2 := ScreenLine{}
  274. sl2.left.marked_up_margin_text = "\x1b[m" + strings.Repeat("━", margin_size)
  275. sl2.left.marked_up_text = strings.Repeat("━", columns-margin_size)
  276. l2.is_full_width = true
  277. l2.screen_lines = append(l2.screen_lines, &sl2)
  278. return append(ans, &ll, &l2)
  279. }
  280. type LogicalLines struct {
  281. lines []*LogicalLine
  282. margin_size, columns int
  283. }
  284. func (self *LogicalLines) At(i int) *LogicalLine { return self.lines[i] }
  285. func (self *LogicalLines) ScreenLineAt(pos ScrollPos) *ScreenLine {
  286. if pos.logical_line < len(self.lines) && pos.logical_line >= 0 {
  287. line := self.lines[pos.logical_line]
  288. if pos.screen_line < len(line.screen_lines) && pos.screen_line >= 0 {
  289. return self.lines[pos.logical_line].screen_lines[pos.screen_line]
  290. }
  291. }
  292. return nil
  293. }
  294. func (self *LogicalLines) Len() int { return len(self.lines) }
  295. func (self *LogicalLines) NumScreenLinesTo(a ScrollPos) (ans int) {
  296. return self.Minus(a, ScrollPos{})
  297. }
  298. // a - b in terms of number of screen lines between the positions
  299. func (self *LogicalLines) Minus(a, b ScrollPos) (delta int) {
  300. if a.logical_line == b.logical_line {
  301. return a.screen_line - b.screen_line
  302. }
  303. amt := 1
  304. if a.Less(b) {
  305. amt = -1
  306. } else {
  307. a, b = b, a
  308. }
  309. for i := a.logical_line; i < utils.Min(len(self.lines), b.logical_line+1); i++ {
  310. line := self.lines[i]
  311. switch i {
  312. case a.logical_line:
  313. delta += utils.Max(0, len(line.screen_lines)-a.screen_line)
  314. case b.logical_line:
  315. delta += b.screen_line
  316. default:
  317. delta += len(line.screen_lines)
  318. }
  319. }
  320. return delta * amt
  321. }
  322. func (self *LogicalLines) IncrementScrollPosBy(pos *ScrollPos, amt int) (delta int) {
  323. if pos.logical_line < 0 || pos.logical_line >= len(self.lines) || amt == 0 {
  324. return
  325. }
  326. one := 1
  327. if amt < 0 {
  328. one = -1
  329. }
  330. for amt != 0 {
  331. line := self.lines[pos.logical_line]
  332. d := line.IncrementScrollPosBy(pos, amt)
  333. if d == 0 {
  334. nlp := pos.logical_line + one
  335. if nlp < 0 || nlp >= len(self.lines) {
  336. break
  337. }
  338. pos.logical_line = nlp
  339. if one > 0 {
  340. pos.screen_line = 0
  341. } else {
  342. pos.screen_line = len(self.lines[nlp].screen_lines) - 1
  343. }
  344. delta += one
  345. amt -= one
  346. } else {
  347. amt -= d
  348. delta += d
  349. }
  350. }
  351. return
  352. }
  353. func human_readable(size int64) string {
  354. divisor, suffix := 1, "B"
  355. for i, candidate := range []string{"B", "KB", "MB", "GB", "TB", "PB", "EB"} {
  356. if size < (1 << ((i + 1) * 10)) {
  357. divisor, suffix = (1 << (i * 10)), candidate
  358. break
  359. }
  360. }
  361. fs := float64(size) / float64(divisor)
  362. s := strconv.FormatFloat(fs, 'f', 2, 64)
  363. if idx := strings.Index(s, "."); idx > -1 {
  364. s = s[:idx+2]
  365. }
  366. if strings.HasSuffix(s, ".0") || strings.HasSuffix(s, ".00") {
  367. idx := strings.IndexByte(s, '.')
  368. s = s[:idx]
  369. }
  370. return s + " " + suffix
  371. }
  372. func image_lines(left_path, right_path string, screen_size screen_size, margin_size int, image_size graphics.Size, ans []*LogicalLine) ([]*LogicalLine, error) {
  373. columns := screen_size.columns
  374. available_cols := columns/2 - margin_size
  375. ll, err := first_binary_line(left_path, right_path, columns, margin_size, func(path string) (string, error) {
  376. sz, err := size_for_path(path)
  377. if err != nil {
  378. return "", err
  379. }
  380. text := fmt.Sprintf("Size: %s", human_readable(sz))
  381. res := image_collection.ResolutionOf(path)
  382. if res.Width > -1 {
  383. text = fmt.Sprintf("Dimensions: %dx%d %s", res.Width, res.Height, text)
  384. }
  385. return text, nil
  386. })
  387. if err != nil {
  388. return nil, err
  389. }
  390. ll.image_lines_offset = len(ll.screen_lines)
  391. do_side := func(path string) []string {
  392. if path == "" {
  393. return nil
  394. }
  395. sz, err := image_collection.GetSizeIfAvailable(path, image_size)
  396. if err == nil {
  397. count := int(math.Ceil(float64(sz.Height) / float64(screen_size.cell_height)))
  398. return utils.Repeat("", count)
  399. }
  400. if errors.Is(err, graphics.ErrNotFound) {
  401. return splitlines("Loading image...", available_cols)
  402. }
  403. return splitlines(fmt.Sprintf("%s", err), available_cols)
  404. }
  405. left_lines := do_side(left_path)
  406. if ll.left_image.count = len(left_lines); ll.left_image.count > 0 {
  407. ll.left_image.key = left_path
  408. }
  409. right_lines := do_side(right_path)
  410. if ll.right_image.count = len(right_lines); ll.right_image.count > 0 {
  411. ll.right_image.key = right_path
  412. }
  413. for i := 0; i < utils.Max(len(left_lines), len(right_lines)); i++ {
  414. sl := ScreenLine{}
  415. if i < len(left_lines) {
  416. sl.left.marked_up_text = left_lines[i]
  417. } else {
  418. sl.left.is_filler = true
  419. }
  420. if i < len(right_lines) {
  421. sl.right.marked_up_text = right_lines[i]
  422. } else {
  423. sl.right.is_filler = true
  424. }
  425. ll.screen_lines = append(ll.screen_lines, &sl)
  426. }
  427. ll.line_type = IMAGE_LINE
  428. return append(ans, ll), nil
  429. }
  430. func first_binary_line(left_path, right_path string, columns, margin_size int, renderer func(path string) (string, error)) (*LogicalLine, error) {
  431. available_cols := columns/2 - margin_size
  432. ll := LogicalLine{
  433. is_change_start: true, line_type: CHANGE_LINE,
  434. left_reference: Reference{path: left_path}, right_reference: Reference{path: right_path},
  435. }
  436. if left_path == "" {
  437. line, err := renderer(right_path)
  438. if err != nil {
  439. return nil, err
  440. }
  441. for _, x := range splitlines(line, available_cols) {
  442. sl := ScreenLine{}
  443. sl.right.marked_up_text = x
  444. sl.left.is_filler = true
  445. ll.screen_lines = append(ll.screen_lines, &sl)
  446. }
  447. } else if right_path == "" {
  448. line, err := renderer(left_path)
  449. if err != nil {
  450. return nil, err
  451. }
  452. for _, x := range splitlines(line, available_cols) {
  453. sl := ScreenLine{}
  454. sl.right.is_filler = true
  455. sl.left.marked_up_text = x
  456. ll.screen_lines = append(ll.screen_lines, &sl)
  457. }
  458. } else {
  459. l, err := renderer(left_path)
  460. if err != nil {
  461. return nil, err
  462. }
  463. r, err := renderer(right_path)
  464. if err != nil {
  465. return nil, err
  466. }
  467. left_lines, right_lines := splitlines(l, available_cols), splitlines(r, available_cols)
  468. for i := 0; i < utils.Max(len(left_lines), len(right_lines)); i++ {
  469. sl := ScreenLine{}
  470. if i < len(left_lines) {
  471. sl.left.marked_up_text = left_lines[i]
  472. }
  473. if i < len(right_lines) {
  474. sl.right.marked_up_text = right_lines[i]
  475. }
  476. ll.screen_lines = append(ll.screen_lines, &sl)
  477. }
  478. }
  479. return &ll, nil
  480. }
  481. func binary_lines(left_path, right_path string, columns, margin_size int, ans []*LogicalLine) (ans2 []*LogicalLine, err error) {
  482. ll, err := first_binary_line(left_path, right_path, columns, margin_size, func(path string) (string, error) {
  483. sz, err := size_for_path(path)
  484. if err != nil {
  485. return "", err
  486. }
  487. return fmt.Sprintf("Binary file: %s", human_readable(sz)), nil
  488. })
  489. if err != nil {
  490. return nil, err
  491. }
  492. return append(ans, ll), nil
  493. }
  494. type DiffData struct {
  495. left_path, right_path string
  496. available_cols, margin_size int
  497. left_lines, right_lines []string
  498. }
  499. func hunk_title(hunk *Hunk) string {
  500. return fmt.Sprintf("@@ -%d,%d +%d,%d @@ %s", hunk.left_start+1, hunk.left_count, hunk.right_start+1, hunk.right_count, hunk.title)
  501. }
  502. func lines_for_context_chunk(data *DiffData, _ int, chunk *Chunk, _ int, ans []*LogicalLine) []*LogicalLine {
  503. for i := 0; i < chunk.left_count; i++ {
  504. left_line_number := chunk.left_start + i
  505. right_line_number := chunk.right_start + i
  506. ll := LogicalLine{line_type: CONTEXT_LINE,
  507. left_reference: Reference{path: data.left_path, linenum: left_line_number + 1},
  508. right_reference: Reference{path: data.right_path, linenum: right_line_number + 1},
  509. }
  510. left_line_number_s := strconv.Itoa(left_line_number + 1)
  511. right_line_number_s := strconv.Itoa(right_line_number + 1)
  512. for _, text := range splitlines(data.left_lines[left_line_number], data.available_cols) {
  513. left_line := HalfScreenLine{marked_up_margin_text: left_line_number_s, marked_up_text: text}
  514. right_line := left_line
  515. if right_line_number_s != left_line_number_s {
  516. right_line = HalfScreenLine{marked_up_margin_text: right_line_number_s, marked_up_text: text}
  517. }
  518. ll.screen_lines = append(ll.screen_lines, &ScreenLine{left_line, right_line})
  519. left_line_number_s, right_line_number_s = "", ""
  520. }
  521. ans = append(ans, &ll)
  522. }
  523. return ans
  524. }
  525. func splitlines(text string, width int) []string {
  526. return style.WrapTextAsLines(text, width, style.WrapOptions{})
  527. }
  528. func render_half_line(line_number int, line, ltype string, available_cols int, center Center, ans []HalfScreenLine) []HalfScreenLine {
  529. size := center.left_size
  530. if ltype != "remove" {
  531. size = center.right_size
  532. }
  533. if size > 0 {
  534. span := center_span(ltype, center.offset, size)
  535. line = sgr.InsertFormatting(line, span)
  536. }
  537. lnum := strconv.Itoa(line_number + 1)
  538. for _, sc := range splitlines(line, available_cols) {
  539. ans = append(ans, HalfScreenLine{marked_up_margin_text: lnum, marked_up_text: sc})
  540. lnum = ""
  541. }
  542. return ans
  543. }
  544. func lines_for_diff_chunk(data *DiffData, _ int, chunk *Chunk, _ int, ans []*LogicalLine) []*LogicalLine {
  545. common := utils.Min(chunk.left_count, chunk.right_count)
  546. ll, rl := make([]HalfScreenLine, 0, 32), make([]HalfScreenLine, 0, 32)
  547. for i := 0; i < utils.Max(chunk.left_count, chunk.right_count); i++ {
  548. ll, rl = ll[:0], rl[:0]
  549. var center Center
  550. left_lnum, right_lnum := 0, 0
  551. if i < len(chunk.centers) {
  552. center = chunk.centers[i]
  553. }
  554. if i < chunk.left_count {
  555. left_lnum = chunk.left_start + i
  556. ll = render_half_line(left_lnum, data.left_lines[left_lnum], "remove", data.available_cols, center, ll)
  557. left_lnum++
  558. }
  559. if i < chunk.right_count {
  560. right_lnum = chunk.right_start + i
  561. rl = render_half_line(right_lnum, data.right_lines[right_lnum], "add", data.available_cols, center, rl)
  562. right_lnum++
  563. }
  564. if i < common {
  565. extra := len(ll) - len(rl)
  566. if extra < 0 {
  567. ll = append(ll, utils.Repeat(HalfScreenLine{}, -extra)...)
  568. } else if extra > 0 {
  569. rl = append(rl, utils.Repeat(HalfScreenLine{}, extra)...)
  570. }
  571. } else {
  572. if len(ll) > 0 {
  573. rl = append(rl, utils.Repeat(HalfScreenLine{is_filler: true}, len(ll))...)
  574. } else if len(rl) > 0 {
  575. ll = append(ll, utils.Repeat(HalfScreenLine{is_filler: true}, len(rl))...)
  576. }
  577. }
  578. logline := LogicalLine{
  579. line_type: CHANGE_LINE, is_change_start: i == 0,
  580. left_reference: Reference{path: data.left_path, linenum: left_lnum},
  581. right_reference: Reference{path: data.left_path, linenum: right_lnum},
  582. }
  583. for l := 0; l < len(ll); l++ {
  584. logline.screen_lines = append(logline.screen_lines, &ScreenLine{left: ll[l], right: rl[l]})
  585. }
  586. ans = append(ans, &logline)
  587. }
  588. return ans
  589. }
  590. func lines_for_diff(left_path string, right_path string, patch *Patch, columns, margin_size int, ans []*LogicalLine) (result []*LogicalLine, err error) {
  591. ht := LogicalLine{
  592. line_type: HUNK_TITLE_LINE,
  593. left_reference: Reference{path: left_path}, right_reference: Reference{path: right_path},
  594. is_full_width: true,
  595. }
  596. if patch.Len() == 0 {
  597. txt := "The files are identical"
  598. if lstat, err := os.Stat(left_path); err == nil {
  599. if rstat, err := os.Stat(right_path); err == nil {
  600. if lstat.Mode() != rstat.Mode() {
  601. txt = fmt.Sprintf("Mode changed: %s to %s", lstat.Mode(), rstat.Mode())
  602. }
  603. }
  604. }
  605. for _, line := range splitlines(txt, columns-margin_size) {
  606. sl := ScreenLine{}
  607. sl.left.marked_up_text = line
  608. ht.screen_lines = append(ht.screen_lines, &sl)
  609. }
  610. ht.line_type = EMPTY_LINE
  611. ht.is_full_width = true
  612. return append(ans, &ht), nil
  613. }
  614. available_cols := columns/2 - margin_size
  615. data := DiffData{left_path: left_path, right_path: right_path, available_cols: available_cols, margin_size: margin_size}
  616. if left_path != "" {
  617. data.left_lines, err = highlighted_lines_for_path(left_path)
  618. if err != nil {
  619. return
  620. }
  621. }
  622. if right_path != "" {
  623. data.right_lines, err = highlighted_lines_for_path(right_path)
  624. if err != nil {
  625. return
  626. }
  627. }
  628. for hunk_num, hunk := range patch.all_hunks {
  629. htl := ht
  630. htl.left_reference.linenum = hunk.left_start + 1
  631. htl.right_reference.linenum = hunk.right_start + 1
  632. for _, line := range splitlines(hunk_title(hunk), columns-margin_size) {
  633. sl := ScreenLine{}
  634. sl.left.marked_up_text = line
  635. htl.screen_lines = append(htl.screen_lines, &sl)
  636. }
  637. ans = append(ans, &htl)
  638. for cnum, chunk := range hunk.chunks {
  639. if chunk.is_context {
  640. ans = lines_for_context_chunk(&data, hunk_num, chunk, cnum, ans)
  641. } else {
  642. ans = lines_for_diff_chunk(&data, hunk_num, chunk, cnum, ans)
  643. }
  644. }
  645. }
  646. return ans, nil
  647. }
  648. func all_lines(path string, columns, margin_size int, is_add bool, ans []*LogicalLine) ([]*LogicalLine, error) {
  649. available_cols := columns/2 - margin_size
  650. ltype := `add`
  651. ll := LogicalLine{line_type: CHANGE_LINE}
  652. if !is_add {
  653. ltype = `remove`
  654. ll.left_reference.path = path
  655. } else {
  656. ll.right_reference.path = path
  657. }
  658. lines, err := highlighted_lines_for_path(path)
  659. if err != nil {
  660. return nil, err
  661. }
  662. var msg_lines []string
  663. if is_add {
  664. msg_lines = splitlines(`This file was added`, available_cols)
  665. } else {
  666. msg_lines = splitlines(`This file was removed`, available_cols)
  667. }
  668. for line_number, line := range lines {
  669. hlines := make([]HalfScreenLine, 0, 8)
  670. hlines = render_half_line(line_number, line, ltype, available_cols, Center{}, hlines)
  671. l := ll
  672. if is_add {
  673. l.right_reference.linenum = line_number + 1
  674. } else {
  675. l.left_reference.linenum = line_number + 1
  676. }
  677. l.is_change_start = line_number == 0
  678. for i, hl := range hlines {
  679. sl := ScreenLine{}
  680. if is_add {
  681. sl.right = hl
  682. if len(msg_lines) > 0 {
  683. sl.left.marked_up_text = msg_lines[i]
  684. sl.left.is_filler = true
  685. msg_lines = msg_lines[1:]
  686. } else {
  687. sl.left.is_filler = true
  688. }
  689. } else {
  690. sl.left = hl
  691. if len(msg_lines) > 0 {
  692. sl.right.marked_up_text = msg_lines[i]
  693. sl.right.is_filler = true
  694. msg_lines = msg_lines[1:]
  695. } else {
  696. sl.right.is_filler = true
  697. }
  698. }
  699. l.screen_lines = append(l.screen_lines, &sl)
  700. }
  701. ans = append(ans, &l)
  702. }
  703. return ans, nil
  704. }
  705. func rename_lines(path, other_path string, columns, margin_size int, ans []*LogicalLine) ([]*LogicalLine, error) {
  706. ll := LogicalLine{
  707. left_reference: Reference{path: path}, right_reference: Reference{path: other_path},
  708. line_type: CHANGE_LINE, is_change_start: true, is_full_width: true}
  709. 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) {
  710. sl := ScreenLine{}
  711. sl.right.marked_up_text = line
  712. ll.screen_lines = append(ll.screen_lines, &sl)
  713. }
  714. return append(ans, &ll), nil
  715. }
  716. func render(collection *Collection, diff_map map[string]*Patch, screen_size screen_size, largest_line_number int, image_size graphics.Size) (result *LogicalLines, err error) {
  717. margin_size := utils.Max(3, len(strconv.Itoa(largest_line_number))+1)
  718. ans := make([]*LogicalLine, 0, 1024)
  719. columns := screen_size.columns
  720. err = collection.Apply(func(path, item_type, changed_path string) error {
  721. ans = title_lines(path, changed_path, columns, margin_size, ans)
  722. defer func() {
  723. ans = append(ans, &LogicalLine{line_type: EMPTY_LINE, screen_lines: []*ScreenLine{{}}})
  724. }()
  725. is_binary := !is_path_text(path)
  726. if !is_binary && item_type == `diff` && !is_path_text(changed_path) {
  727. is_binary = true
  728. }
  729. is_img := is_binary && is_image(path) || (item_type == `diff` && is_image(changed_path))
  730. _ = is_img
  731. switch item_type {
  732. case "diff":
  733. if is_binary {
  734. if is_img {
  735. ans, err = image_lines(path, changed_path, screen_size, margin_size, image_size, ans)
  736. } else {
  737. ans, err = binary_lines(path, changed_path, columns, margin_size, ans)
  738. }
  739. } else {
  740. ans, err = lines_for_diff(path, changed_path, diff_map[path], columns, margin_size, ans)
  741. }
  742. if err != nil {
  743. return err
  744. }
  745. case "add":
  746. if is_binary {
  747. if is_img {
  748. ans, err = image_lines("", path, screen_size, margin_size, image_size, ans)
  749. } else {
  750. ans, err = binary_lines("", path, columns, margin_size, ans)
  751. }
  752. } else {
  753. ans, err = all_lines(path, columns, margin_size, true, ans)
  754. }
  755. if err != nil {
  756. return err
  757. }
  758. case "removal":
  759. if is_binary {
  760. if is_img {
  761. ans, err = image_lines(path, "", screen_size, margin_size, image_size, ans)
  762. } else {
  763. ans, err = binary_lines(path, "", columns, margin_size, ans)
  764. }
  765. } else {
  766. ans, err = all_lines(path, columns, margin_size, false, ans)
  767. }
  768. if err != nil {
  769. return err
  770. }
  771. case "rename":
  772. ans, err = rename_lines(path, changed_path, columns, margin_size, ans)
  773. if err != nil {
  774. return err
  775. }
  776. default:
  777. return fmt.Errorf("Unknown change type: %#v", item_type)
  778. }
  779. return nil
  780. })
  781. var ll []*LogicalLine
  782. if len(ans) > 1 {
  783. ll = ans[:len(ans)-1]
  784. } else {
  785. // Having am empty list of lines causes panics later on
  786. ll = []*LogicalLine{{line_type: EMPTY_LINE, screen_lines: []*ScreenLine{{}}}}
  787. }
  788. return &LogicalLines{lines: ll, margin_size: margin_size, columns: columns}, err
  789. }