render.go 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771
  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. func create_formatters() {
  146. ctx := style.Context{AllowEscapeCodes: true}
  147. only_open := func(x string) string {
  148. ans := ctx.SprintFunc(x)("|")
  149. ans, _, _ = strings.Cut(ans, "|")
  150. return ans
  151. }
  152. format_as_sgr.filler = only_open("bg=" + conf.Filler_bg.AsRGBSharp())
  153. if conf.Margin_filler_bg.IsSet {
  154. format_as_sgr.margin_filler = only_open("bg=" + conf.Margin_filler_bg.Color.AsRGBSharp())
  155. } else {
  156. format_as_sgr.margin_filler = only_open("bg=" + conf.Filler_bg.AsRGBSharp())
  157. }
  158. format_as_sgr.added = only_open("bg=" + conf.Added_bg.AsRGBSharp())
  159. format_as_sgr.added_margin = only_open(fmt.Sprintf("fg=%s bg=%s", conf.Margin_fg.AsRGBSharp(), conf.Added_margin_bg.AsRGBSharp()))
  160. format_as_sgr.removed = only_open("bg=" + conf.Removed_bg.AsRGBSharp())
  161. format_as_sgr.removed_margin = only_open(fmt.Sprintf("fg=%s bg=%s", conf.Margin_fg.AsRGBSharp(), conf.Removed_margin_bg.AsRGBSharp()))
  162. format_as_sgr.title = only_open(fmt.Sprintf("fg=%s bg=%s bold", conf.Title_fg.AsRGBSharp(), conf.Title_bg.AsRGBSharp()))
  163. format_as_sgr.margin = only_open(fmt.Sprintf("fg=%s bg=%s", conf.Margin_fg.AsRGBSharp(), conf.Margin_bg.AsRGBSharp()))
  164. format_as_sgr.hunk = only_open(fmt.Sprintf("fg=%s bg=%s", conf.Margin_fg.AsRGBSharp(), conf.Hunk_bg.AsRGBSharp()))
  165. format_as_sgr.hunk_margin = only_open(fmt.Sprintf("fg=%s bg=%s", conf.Margin_fg.AsRGBSharp(), conf.Hunk_margin_bg.AsRGBSharp()))
  166. format_as_sgr.search = only_open(fmt.Sprintf("fg=%s bg=%s", conf.Search_fg.AsRGBSharp(), conf.Search_bg.AsRGBSharp()))
  167. statusline_format = ctx.SprintFunc(fmt.Sprintf("fg=%s", conf.Margin_fg.AsRGBSharp()))
  168. added_count_format = ctx.SprintFunc(fmt.Sprintf("fg=%s", conf.Highlight_added_bg.AsRGBSharp()))
  169. removed_count_format = ctx.SprintFunc(fmt.Sprintf("fg=%s", conf.Highlight_removed_bg.AsRGBSharp()))
  170. message_format = ctx.SprintFunc("bold")
  171. if conf.Select_fg.IsSet {
  172. format_as_sgr.selection = only_open(fmt.Sprintf("fg=%s bg=%s", conf.Select_fg.Color.AsRGBSharp(), conf.Select_bg.AsRGBSharp()))
  173. } else {
  174. format_as_sgr.selection = only_open("bg=" + conf.Select_bg.AsRGBSharp())
  175. }
  176. }
  177. func center_span(ltype string, offset, size int) *sgr.Span {
  178. ans := sgr.NewSpan(offset, size)
  179. switch ltype {
  180. case "add":
  181. ans.SetBackground(conf.Highlight_added_bg).SetClosingBackground(conf.Added_bg)
  182. case "remove":
  183. ans.SetBackground(conf.Highlight_removed_bg).SetClosingBackground(conf.Removed_bg)
  184. }
  185. return ans
  186. }
  187. func title_lines(left_path, right_path string, columns, margin_size int, ans []*LogicalLine) []*LogicalLine {
  188. left_name, right_name := path_name_map[left_path], path_name_map[right_path]
  189. available_cols := columns/2 - margin_size
  190. ll := LogicalLine{
  191. line_type: TITLE_LINE,
  192. left_reference: Reference{path: left_path}, right_reference: Reference{path: right_path},
  193. }
  194. sl := ScreenLine{}
  195. if right_name != "" && right_name != left_name {
  196. sl.left.marked_up_text = format_as_sgr.title + fit_in(sanitize(left_name), available_cols)
  197. sl.right.marked_up_text = format_as_sgr.title + fit_in(sanitize(right_name), available_cols)
  198. } else {
  199. sl.left.marked_up_text = format_as_sgr.title + fit_in(sanitize(left_name), columns-margin_size)
  200. ll.is_full_width = true
  201. }
  202. l2 := ll
  203. l2.line_type = EMPTY_LINE
  204. ll.screen_lines = append(ll.screen_lines, &sl)
  205. sl2 := ScreenLine{}
  206. sl2.left.marked_up_margin_text = "\x1b[m" + strings.Repeat("━", margin_size)
  207. sl2.left.marked_up_text = strings.Repeat("━", columns-margin_size)
  208. l2.is_full_width = true
  209. l2.screen_lines = append(l2.screen_lines, &sl2)
  210. return append(ans, &ll, &l2)
  211. }
  212. type LogicalLines struct {
  213. lines []*LogicalLine
  214. margin_size, columns int
  215. }
  216. func (self *LogicalLines) At(i int) *LogicalLine { return self.lines[i] }
  217. func (self *LogicalLines) ScreenLineAt(pos ScrollPos) *ScreenLine {
  218. if pos.logical_line < len(self.lines) && pos.logical_line >= 0 {
  219. line := self.lines[pos.logical_line]
  220. if pos.screen_line < len(line.screen_lines) && pos.screen_line >= 0 {
  221. return self.lines[pos.logical_line].screen_lines[pos.screen_line]
  222. }
  223. }
  224. return nil
  225. }
  226. func (self *LogicalLines) Len() int { return len(self.lines) }
  227. func (self *LogicalLines) NumScreenLinesTo(a ScrollPos) (ans int) {
  228. return self.Minus(a, ScrollPos{})
  229. }
  230. // a - b in terms of number of screen lines between the positions
  231. func (self *LogicalLines) Minus(a, b ScrollPos) (delta int) {
  232. if a.logical_line == b.logical_line {
  233. return a.screen_line - b.screen_line
  234. }
  235. amt := 1
  236. if a.Less(b) {
  237. amt = -1
  238. } else {
  239. a, b = b, a
  240. }
  241. for i := a.logical_line; i < utils.Min(len(self.lines), b.logical_line+1); i++ {
  242. line := self.lines[i]
  243. switch i {
  244. case a.logical_line:
  245. delta += utils.Max(0, len(line.screen_lines)-a.screen_line)
  246. case b.logical_line:
  247. delta += b.screen_line
  248. default:
  249. delta += len(line.screen_lines)
  250. }
  251. }
  252. return delta * amt
  253. }
  254. func (self *LogicalLines) IncrementScrollPosBy(pos *ScrollPos, amt int) (delta int) {
  255. if pos.logical_line < 0 || pos.logical_line >= len(self.lines) || amt == 0 {
  256. return
  257. }
  258. one := 1
  259. if amt < 0 {
  260. one = -1
  261. }
  262. for amt != 0 {
  263. line := self.lines[pos.logical_line]
  264. d := line.IncrementScrollPosBy(pos, amt)
  265. if d == 0 {
  266. nlp := pos.logical_line + one
  267. if nlp < 0 || nlp >= len(self.lines) {
  268. break
  269. }
  270. pos.logical_line = nlp
  271. if one > 0 {
  272. pos.screen_line = 0
  273. } else {
  274. pos.screen_line = len(self.lines[nlp].screen_lines) - 1
  275. }
  276. delta += one
  277. amt -= one
  278. } else {
  279. amt -= d
  280. delta += d
  281. }
  282. }
  283. return
  284. }
  285. func human_readable(size int64) string {
  286. divisor, suffix := 1, "B"
  287. for i, candidate := range []string{"B", "KB", "MB", "GB", "TB", "PB", "EB"} {
  288. if size < (1 << ((i + 1) * 10)) {
  289. divisor, suffix = (1 << (i * 10)), candidate
  290. break
  291. }
  292. }
  293. fs := float64(size) / float64(divisor)
  294. s := strconv.FormatFloat(fs, 'f', 2, 64)
  295. if idx := strings.Index(s, "."); idx > -1 {
  296. s = s[:idx+2]
  297. }
  298. if strings.HasSuffix(s, ".0") || strings.HasSuffix(s, ".00") {
  299. idx := strings.IndexByte(s, '.')
  300. s = s[:idx]
  301. }
  302. return s + " " + suffix
  303. }
  304. func image_lines(left_path, right_path string, screen_size screen_size, margin_size int, image_size graphics.Size, ans []*LogicalLine) ([]*LogicalLine, error) {
  305. columns := screen_size.columns
  306. available_cols := columns/2 - margin_size
  307. ll, err := first_binary_line(left_path, right_path, columns, margin_size, func(path string) (string, error) {
  308. sz, err := size_for_path(path)
  309. if err != nil {
  310. return "", err
  311. }
  312. text := fmt.Sprintf("Size: %s", human_readable(sz))
  313. res := image_collection.ResolutionOf(path)
  314. if res.Width > -1 {
  315. text = fmt.Sprintf("Dimensions: %dx%d %s", res.Width, res.Height, text)
  316. }
  317. return text, nil
  318. })
  319. if err != nil {
  320. return nil, err
  321. }
  322. ll.image_lines_offset = len(ll.screen_lines)
  323. do_side := func(path string) []string {
  324. if path == "" {
  325. return nil
  326. }
  327. sz, err := image_collection.GetSizeIfAvailable(path, image_size)
  328. if err == nil {
  329. count := int(math.Ceil(float64(sz.Height) / float64(screen_size.cell_height)))
  330. return utils.Repeat("", count)
  331. }
  332. if errors.Is(err, graphics.ErrNotFound) {
  333. return splitlines("Loading image...", available_cols)
  334. }
  335. return splitlines(fmt.Sprintf("%s", err), available_cols)
  336. }
  337. left_lines := do_side(left_path)
  338. if ll.left_image.count = len(left_lines); ll.left_image.count > 0 {
  339. ll.left_image.key = left_path
  340. }
  341. right_lines := do_side(right_path)
  342. if ll.right_image.count = len(right_lines); ll.right_image.count > 0 {
  343. ll.right_image.key = right_path
  344. }
  345. for i := 0; i < utils.Max(len(left_lines), len(right_lines)); i++ {
  346. sl := ScreenLine{}
  347. if i < len(left_lines) {
  348. sl.left.marked_up_text = left_lines[i]
  349. } else {
  350. sl.left.is_filler = true
  351. }
  352. if i < len(right_lines) {
  353. sl.right.marked_up_text = right_lines[i]
  354. } else {
  355. sl.right.is_filler = true
  356. }
  357. ll.screen_lines = append(ll.screen_lines, &sl)
  358. }
  359. ll.line_type = IMAGE_LINE
  360. return append(ans, ll), nil
  361. }
  362. func first_binary_line(left_path, right_path string, columns, margin_size int, renderer func(path string) (string, error)) (*LogicalLine, error) {
  363. available_cols := columns/2 - margin_size
  364. ll := LogicalLine{
  365. is_change_start: true, line_type: CHANGE_LINE,
  366. left_reference: Reference{path: left_path}, right_reference: Reference{path: right_path},
  367. }
  368. if left_path == "" {
  369. line, err := renderer(right_path)
  370. if err != nil {
  371. return nil, err
  372. }
  373. for _, x := range splitlines(line, available_cols) {
  374. sl := ScreenLine{}
  375. sl.right.marked_up_text = x
  376. sl.left.is_filler = true
  377. ll.screen_lines = append(ll.screen_lines, &sl)
  378. }
  379. } else if right_path == "" {
  380. line, err := renderer(left_path)
  381. if err != nil {
  382. return nil, err
  383. }
  384. for _, x := range splitlines(line, available_cols) {
  385. sl := ScreenLine{}
  386. sl.right.is_filler = true
  387. sl.left.marked_up_text = x
  388. ll.screen_lines = append(ll.screen_lines, &sl)
  389. }
  390. } else {
  391. l, err := renderer(left_path)
  392. if err != nil {
  393. return nil, err
  394. }
  395. r, err := renderer(right_path)
  396. if err != nil {
  397. return nil, err
  398. }
  399. left_lines, right_lines := splitlines(l, available_cols), splitlines(r, available_cols)
  400. for i := 0; i < utils.Max(len(left_lines), len(right_lines)); i++ {
  401. sl := ScreenLine{}
  402. if i < len(left_lines) {
  403. sl.left.marked_up_text = left_lines[i]
  404. }
  405. if i < len(right_lines) {
  406. sl.right.marked_up_text = right_lines[i]
  407. }
  408. ll.screen_lines = append(ll.screen_lines, &sl)
  409. }
  410. }
  411. return &ll, nil
  412. }
  413. func binary_lines(left_path, right_path string, columns, margin_size int, ans []*LogicalLine) (ans2 []*LogicalLine, err error) {
  414. ll, err := first_binary_line(left_path, right_path, columns, margin_size, func(path string) (string, error) {
  415. sz, err := size_for_path(path)
  416. if err != nil {
  417. return "", err
  418. }
  419. return fmt.Sprintf("Binary file: %s", human_readable(sz)), nil
  420. })
  421. if err != nil {
  422. return nil, err
  423. }
  424. return append(ans, ll), nil
  425. }
  426. type DiffData struct {
  427. left_path, right_path string
  428. available_cols, margin_size int
  429. left_lines, right_lines []string
  430. }
  431. func hunk_title(hunk *Hunk) string {
  432. return fmt.Sprintf("@@ -%d,%d +%d,%d @@ %s", hunk.left_start+1, hunk.left_count, hunk.right_start+1, hunk.right_count, hunk.title)
  433. }
  434. func lines_for_context_chunk(data *DiffData, hunk_num int, chunk *Chunk, chunk_num int, ans []*LogicalLine) []*LogicalLine {
  435. for i := 0; i < chunk.left_count; i++ {
  436. left_line_number := chunk.left_start + i
  437. right_line_number := chunk.right_start + i
  438. ll := LogicalLine{line_type: CONTEXT_LINE,
  439. left_reference: Reference{path: data.left_path, linenum: left_line_number + 1},
  440. right_reference: Reference{path: data.right_path, linenum: right_line_number + 1},
  441. }
  442. left_line_number_s := strconv.Itoa(left_line_number + 1)
  443. right_line_number_s := strconv.Itoa(right_line_number + 1)
  444. for _, text := range splitlines(data.left_lines[left_line_number], data.available_cols) {
  445. left_line := HalfScreenLine{marked_up_margin_text: left_line_number_s, marked_up_text: text}
  446. right_line := left_line
  447. if right_line_number_s != left_line_number_s {
  448. right_line = HalfScreenLine{marked_up_margin_text: right_line_number_s, marked_up_text: text}
  449. }
  450. ll.screen_lines = append(ll.screen_lines, &ScreenLine{left_line, right_line})
  451. left_line_number_s, right_line_number_s = "", ""
  452. }
  453. ans = append(ans, &ll)
  454. }
  455. return ans
  456. }
  457. func splitlines(text string, width int) []string {
  458. return style.WrapTextAsLines(text, width, style.WrapOptions{})
  459. }
  460. func render_half_line(line_number int, line, ltype string, available_cols int, center Center, ans []HalfScreenLine) []HalfScreenLine {
  461. size := center.left_size
  462. if ltype != "remove" {
  463. size = center.right_size
  464. }
  465. if size > 0 {
  466. span := center_span(ltype, center.offset, size)
  467. line = sgr.InsertFormatting(line, span)
  468. }
  469. lnum := strconv.Itoa(line_number + 1)
  470. for _, sc := range splitlines(line, available_cols) {
  471. ans = append(ans, HalfScreenLine{marked_up_margin_text: lnum, marked_up_text: sc})
  472. lnum = ""
  473. }
  474. return ans
  475. }
  476. func lines_for_diff_chunk(data *DiffData, hunk_num int, chunk *Chunk, chunk_num int, ans []*LogicalLine) []*LogicalLine {
  477. common := utils.Min(chunk.left_count, chunk.right_count)
  478. ll, rl := make([]HalfScreenLine, 0, 32), make([]HalfScreenLine, 0, 32)
  479. for i := 0; i < utils.Max(chunk.left_count, chunk.right_count); i++ {
  480. ll, rl = ll[:0], rl[:0]
  481. var center Center
  482. left_lnum, right_lnum := 0, 0
  483. if i < len(chunk.centers) {
  484. center = chunk.centers[i]
  485. }
  486. if i < chunk.left_count {
  487. left_lnum = chunk.left_start + i
  488. ll = render_half_line(left_lnum, data.left_lines[left_lnum], "remove", data.available_cols, center, ll)
  489. left_lnum++
  490. }
  491. if i < chunk.right_count {
  492. right_lnum = chunk.right_start + i
  493. rl = render_half_line(right_lnum, data.right_lines[right_lnum], "add", data.available_cols, center, rl)
  494. right_lnum++
  495. }
  496. if i < common {
  497. extra := len(ll) - len(rl)
  498. if extra < 0 {
  499. ll = append(ll, utils.Repeat(HalfScreenLine{}, -extra)...)
  500. } else if extra > 0 {
  501. rl = append(rl, utils.Repeat(HalfScreenLine{}, extra)...)
  502. }
  503. } else {
  504. if len(ll) > 0 {
  505. rl = append(rl, utils.Repeat(HalfScreenLine{is_filler: true}, len(ll))...)
  506. } else if len(rl) > 0 {
  507. ll = append(ll, utils.Repeat(HalfScreenLine{is_filler: true}, len(rl))...)
  508. }
  509. }
  510. logline := LogicalLine{
  511. line_type: CHANGE_LINE, is_change_start: i == 0,
  512. left_reference: Reference{path: data.left_path, linenum: left_lnum},
  513. right_reference: Reference{path: data.left_path, linenum: right_lnum},
  514. }
  515. for l := 0; l < len(ll); l++ {
  516. logline.screen_lines = append(logline.screen_lines, &ScreenLine{left: ll[l], right: rl[l]})
  517. }
  518. ans = append(ans, &logline)
  519. }
  520. return ans
  521. }
  522. func lines_for_diff(left_path string, right_path string, patch *Patch, columns, margin_size int, ans []*LogicalLine) (result []*LogicalLine, err error) {
  523. ht := LogicalLine{
  524. line_type: HUNK_TITLE_LINE,
  525. left_reference: Reference{path: left_path}, right_reference: Reference{path: right_path},
  526. is_full_width: true,
  527. }
  528. if patch.Len() == 0 {
  529. txt := "The files are identical"
  530. if lstat, err := os.Stat(left_path); err == nil {
  531. if rstat, err := os.Stat(right_path); err == nil {
  532. if lstat.Mode() != rstat.Mode() {
  533. txt = fmt.Sprintf("Mode changed: %s to %s", lstat.Mode(), rstat.Mode())
  534. }
  535. }
  536. }
  537. for _, line := range splitlines(txt, columns-margin_size) {
  538. sl := ScreenLine{}
  539. sl.left.marked_up_text = line
  540. ht.screen_lines = append(ht.screen_lines, &sl)
  541. }
  542. ht.line_type = EMPTY_LINE
  543. ht.is_full_width = true
  544. return append(ans, &ht), nil
  545. }
  546. available_cols := columns/2 - margin_size
  547. data := DiffData{left_path: left_path, right_path: right_path, available_cols: available_cols, margin_size: margin_size}
  548. if left_path != "" {
  549. data.left_lines, err = highlighted_lines_for_path(left_path)
  550. if err != nil {
  551. return
  552. }
  553. }
  554. if right_path != "" {
  555. data.right_lines, err = highlighted_lines_for_path(right_path)
  556. if err != nil {
  557. return
  558. }
  559. }
  560. for hunk_num, hunk := range patch.all_hunks {
  561. htl := ht
  562. htl.left_reference.linenum = hunk.left_start + 1
  563. htl.right_reference.linenum = hunk.right_start + 1
  564. for _, line := range splitlines(hunk_title(hunk), columns-margin_size) {
  565. sl := ScreenLine{}
  566. sl.left.marked_up_text = line
  567. htl.screen_lines = append(htl.screen_lines, &sl)
  568. }
  569. ans = append(ans, &htl)
  570. for cnum, chunk := range hunk.chunks {
  571. if chunk.is_context {
  572. ans = lines_for_context_chunk(&data, hunk_num, chunk, cnum, ans)
  573. } else {
  574. ans = lines_for_diff_chunk(&data, hunk_num, chunk, cnum, ans)
  575. }
  576. }
  577. }
  578. return ans, nil
  579. }
  580. func all_lines(path string, columns, margin_size int, is_add bool, ans []*LogicalLine) ([]*LogicalLine, error) {
  581. available_cols := columns/2 - margin_size
  582. ltype := `add`
  583. ll := LogicalLine{line_type: CHANGE_LINE}
  584. if !is_add {
  585. ltype = `remove`
  586. ll.left_reference.path = path
  587. } else {
  588. ll.right_reference.path = path
  589. }
  590. lines, err := highlighted_lines_for_path(path)
  591. if err != nil {
  592. return nil, err
  593. }
  594. var msg_lines []string
  595. if is_add {
  596. msg_lines = splitlines(`This file was added`, available_cols)
  597. } else {
  598. msg_lines = splitlines(`This file was removed`, available_cols)
  599. }
  600. for line_number, line := range lines {
  601. hlines := make([]HalfScreenLine, 0, 8)
  602. hlines = render_half_line(line_number, line, ltype, available_cols, Center{}, hlines)
  603. l := ll
  604. if is_add {
  605. l.right_reference.linenum = line_number + 1
  606. } else {
  607. l.left_reference.linenum = line_number + 1
  608. }
  609. l.is_change_start = line_number == 0
  610. for i, hl := range hlines {
  611. sl := ScreenLine{}
  612. if is_add {
  613. sl.right = hl
  614. if len(msg_lines) > 0 {
  615. sl.left.marked_up_text = msg_lines[i]
  616. sl.left.is_filler = true
  617. msg_lines = msg_lines[1:]
  618. } else {
  619. sl.left.is_filler = true
  620. }
  621. } else {
  622. sl.left = hl
  623. if len(msg_lines) > 0 {
  624. sl.right.marked_up_text = msg_lines[i]
  625. sl.right.is_filler = true
  626. msg_lines = msg_lines[1:]
  627. } else {
  628. sl.right.is_filler = true
  629. }
  630. }
  631. l.screen_lines = append(l.screen_lines, &sl)
  632. }
  633. ans = append(ans, &l)
  634. }
  635. return ans, nil
  636. }
  637. func rename_lines(path, other_path string, columns, margin_size int, ans []*LogicalLine) ([]*LogicalLine, error) {
  638. ll := LogicalLine{
  639. left_reference: Reference{path: path}, right_reference: Reference{path: other_path},
  640. line_type: CHANGE_LINE, is_change_start: true, is_full_width: true}
  641. 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) {
  642. sl := ScreenLine{}
  643. sl.right.marked_up_text = line
  644. ll.screen_lines = append(ll.screen_lines, &sl)
  645. }
  646. return append(ans, &ll), nil
  647. }
  648. 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) {
  649. margin_size := utils.Max(3, len(strconv.Itoa(largest_line_number))+1)
  650. ans := make([]*LogicalLine, 0, 1024)
  651. columns := screen_size.columns
  652. err = collection.Apply(func(path, item_type, changed_path string) error {
  653. ans = title_lines(path, changed_path, columns, margin_size, ans)
  654. defer func() {
  655. ans = append(ans, &LogicalLine{line_type: EMPTY_LINE, screen_lines: []*ScreenLine{{}}})
  656. }()
  657. is_binary := !is_path_text(path)
  658. if !is_binary && item_type == `diff` && !is_path_text(changed_path) {
  659. is_binary = true
  660. }
  661. is_img := is_binary && is_image(path) || (item_type == `diff` && is_image(changed_path))
  662. _ = is_img
  663. switch item_type {
  664. case "diff":
  665. if is_binary {
  666. if is_img {
  667. ans, err = image_lines(path, changed_path, screen_size, margin_size, image_size, ans)
  668. } else {
  669. ans, err = binary_lines(path, changed_path, columns, margin_size, ans)
  670. }
  671. } else {
  672. ans, err = lines_for_diff(path, changed_path, diff_map[path], columns, margin_size, ans)
  673. }
  674. if err != nil {
  675. return err
  676. }
  677. case "add":
  678. if is_binary {
  679. if is_img {
  680. ans, err = image_lines("", path, screen_size, margin_size, image_size, ans)
  681. } else {
  682. ans, err = binary_lines("", path, columns, margin_size, ans)
  683. }
  684. } else {
  685. ans, err = all_lines(path, columns, margin_size, true, ans)
  686. }
  687. if err != nil {
  688. return err
  689. }
  690. case "removal":
  691. if is_binary {
  692. if is_img {
  693. ans, err = image_lines(path, "", screen_size, margin_size, image_size, ans)
  694. } else {
  695. ans, err = binary_lines(path, "", columns, margin_size, ans)
  696. }
  697. } else {
  698. ans, err = all_lines(path, columns, margin_size, false, ans)
  699. }
  700. if err != nil {
  701. return err
  702. }
  703. case "rename":
  704. ans, err = rename_lines(path, changed_path, columns, margin_size, ans)
  705. if err != nil {
  706. return err
  707. }
  708. default:
  709. return fmt.Errorf("Unknown change type: %#v", item_type)
  710. }
  711. return nil
  712. })
  713. var ll []*LogicalLine
  714. if len(ans) > 1 {
  715. ll = ans[:len(ans)-1]
  716. } else {
  717. // Having am empty list of lines causes panics later on
  718. ll = []*LogicalLine{{line_type: EMPTY_LINE, screen_lines: []*ScreenLine{{}}}}
  719. }
  720. return &LogicalLines{lines: ll, margin_size: margin_size, columns: columns}, err
  721. }