diffview.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732
  1. package diffview
  2. import (
  3. "fmt"
  4. "image/color"
  5. "strconv"
  6. "strings"
  7. "github.com/alecthomas/chroma/v2"
  8. "github.com/alecthomas/chroma/v2/lexers"
  9. "github.com/aymanbagabas/go-udiff"
  10. "github.com/aymanbagabas/go-udiff/myers"
  11. "github.com/charmbracelet/lipgloss/v2"
  12. "github.com/charmbracelet/x/ansi"
  13. )
  14. const (
  15. leadingSymbolsSize = 2
  16. lineNumPadding = 1
  17. )
  18. type file struct {
  19. path string
  20. content string
  21. }
  22. type layout int
  23. const (
  24. layoutUnified layout = iota + 1
  25. layoutSplit
  26. )
  27. // DiffView represents a view for displaying differences between two files.
  28. type DiffView struct {
  29. layout layout
  30. before file
  31. after file
  32. contextLines int
  33. lineNumbers bool
  34. height int
  35. width int
  36. xOffset int
  37. yOffset int
  38. infiniteYScroll bool
  39. style Style
  40. tabWidth int
  41. chromaStyle *chroma.Style
  42. isComputed bool
  43. err error
  44. unified udiff.UnifiedDiff
  45. edits []udiff.Edit
  46. splitHunks []splitHunk
  47. totalLines int
  48. codeWidth int
  49. fullCodeWidth int // with leading symbols
  50. extraColOnAfter bool // add extra column on after panel
  51. beforeNumDigits int
  52. afterNumDigits int
  53. }
  54. // New creates a new DiffView with default settings.
  55. func New() *DiffView {
  56. dv := &DiffView{
  57. layout: layoutUnified,
  58. contextLines: udiff.DefaultContextLines,
  59. lineNumbers: true,
  60. tabWidth: 8,
  61. }
  62. dv.style = DefaultDarkStyle()
  63. return dv
  64. }
  65. // Unified sets the layout of the DiffView to unified.
  66. func (dv *DiffView) Unified() *DiffView {
  67. dv.layout = layoutUnified
  68. return dv
  69. }
  70. // Split sets the layout of the DiffView to split (side-by-side).
  71. func (dv *DiffView) Split() *DiffView {
  72. dv.layout = layoutSplit
  73. return dv
  74. }
  75. // Before sets the "before" file for the DiffView.
  76. func (dv *DiffView) Before(path, content string) *DiffView {
  77. dv.before = file{path: path, content: content}
  78. return dv
  79. }
  80. // After sets the "after" file for the DiffView.
  81. func (dv *DiffView) After(path, content string) *DiffView {
  82. dv.after = file{path: path, content: content}
  83. return dv
  84. }
  85. // ContextLines sets the number of context lines for the DiffView.
  86. func (dv *DiffView) ContextLines(contextLines int) *DiffView {
  87. dv.contextLines = contextLines
  88. return dv
  89. }
  90. // Style sets the style for the DiffView.
  91. func (dv *DiffView) Style(style Style) *DiffView {
  92. dv.style = style
  93. return dv
  94. }
  95. // LineNumbers sets whether to display line numbers in the DiffView.
  96. func (dv *DiffView) LineNumbers(lineNumbers bool) *DiffView {
  97. dv.lineNumbers = lineNumbers
  98. return dv
  99. }
  100. // Height sets the height of the DiffView.
  101. func (dv *DiffView) Height(height int) *DiffView {
  102. dv.height = height
  103. return dv
  104. }
  105. // Width sets the width of the DiffView.
  106. func (dv *DiffView) Width(width int) *DiffView {
  107. dv.width = width
  108. return dv
  109. }
  110. // XOffset sets the horizontal offset for the DiffView.
  111. func (dv *DiffView) XOffset(xOffset int) *DiffView {
  112. dv.xOffset = xOffset
  113. return dv
  114. }
  115. // YOffset sets the vertical offset for the DiffView.
  116. func (dv *DiffView) YOffset(yOffset int) *DiffView {
  117. dv.yOffset = yOffset
  118. return dv
  119. }
  120. // InfiniteYScroll allows the YOffset to scroll beyond the last line.
  121. func (dv *DiffView) InfiniteYScroll(infiniteYScroll bool) *DiffView {
  122. dv.infiniteYScroll = infiniteYScroll
  123. return dv
  124. }
  125. // TabWidth sets the tab width. Only relevant for code that contains tabs, like
  126. // Go code.
  127. func (dv *DiffView) TabWidth(tabWidth int) *DiffView {
  128. dv.tabWidth = tabWidth
  129. return dv
  130. }
  131. // ChromaStyle sets the chroma style for syntax highlighting.
  132. // If nil, no syntax highlighting will be applied.
  133. func (dv *DiffView) ChromaStyle(style *chroma.Style) *DiffView {
  134. dv.chromaStyle = style
  135. return dv
  136. }
  137. // String returns the string representation of the DiffView.
  138. func (dv *DiffView) String() string {
  139. dv.replaceTabs()
  140. if err := dv.computeDiff(); err != nil {
  141. return err.Error()
  142. }
  143. dv.convertDiffToSplit()
  144. dv.adjustStyles()
  145. dv.detectNumDigits()
  146. dv.detectTotalLines()
  147. dv.preventInfiniteYScroll()
  148. if dv.width <= 0 {
  149. dv.detectCodeWidth()
  150. } else {
  151. dv.resizeCodeWidth()
  152. }
  153. style := lipgloss.NewStyle()
  154. if dv.width > 0 {
  155. style = style.MaxWidth(dv.width)
  156. }
  157. if dv.height > 0 {
  158. style = style.MaxHeight(dv.height)
  159. }
  160. switch dv.layout {
  161. case layoutUnified:
  162. return style.Render(strings.TrimSuffix(dv.renderUnified(), "\n"))
  163. case layoutSplit:
  164. return style.Render(strings.TrimSuffix(dv.renderSplit(), "\n"))
  165. default:
  166. panic("unknown diffview layout")
  167. }
  168. }
  169. // replaceTabs replaces tabs in the before and after file contents with spaces
  170. // according to the specified tab width.
  171. func (dv *DiffView) replaceTabs() {
  172. spaces := strings.Repeat(" ", dv.tabWidth)
  173. dv.before.content = strings.ReplaceAll(dv.before.content, "\t", spaces)
  174. dv.after.content = strings.ReplaceAll(dv.after.content, "\t", spaces)
  175. }
  176. // computeDiff computes the differences between the "before" and "after" files.
  177. func (dv *DiffView) computeDiff() error {
  178. if dv.isComputed {
  179. return dv.err
  180. }
  181. dv.isComputed = true
  182. dv.edits = myers.ComputeEdits( //nolint:staticcheck
  183. dv.before.content,
  184. dv.after.content,
  185. )
  186. dv.unified, dv.err = udiff.ToUnifiedDiff(
  187. dv.before.path,
  188. dv.after.path,
  189. dv.before.content,
  190. dv.edits,
  191. dv.contextLines,
  192. )
  193. return dv.err
  194. }
  195. // convertDiffToSplit converts the unified diff to a split diff if the layout is
  196. // set to split.
  197. func (dv *DiffView) convertDiffToSplit() {
  198. if dv.layout != layoutSplit {
  199. return
  200. }
  201. dv.splitHunks = make([]splitHunk, len(dv.unified.Hunks))
  202. for i, h := range dv.unified.Hunks {
  203. dv.splitHunks[i] = hunkToSplit(h)
  204. }
  205. }
  206. // adjustStyles adjusts adds padding and alignment to the styles.
  207. func (dv *DiffView) adjustStyles() {
  208. setPadding := func(s lipgloss.Style) lipgloss.Style {
  209. return s.Padding(0, lineNumPadding).Align(lipgloss.Right)
  210. }
  211. dv.style.MissingLine.LineNumber = setPadding(dv.style.MissingLine.LineNumber)
  212. dv.style.DividerLine.LineNumber = setPadding(dv.style.DividerLine.LineNumber)
  213. dv.style.EqualLine.LineNumber = setPadding(dv.style.EqualLine.LineNumber)
  214. dv.style.InsertLine.LineNumber = setPadding(dv.style.InsertLine.LineNumber)
  215. dv.style.DeleteLine.LineNumber = setPadding(dv.style.DeleteLine.LineNumber)
  216. }
  217. // detectNumDigits calculates the maximum number of digits needed for before and
  218. // after line numbers.
  219. func (dv *DiffView) detectNumDigits() {
  220. dv.beforeNumDigits = 0
  221. dv.afterNumDigits = 0
  222. for _, h := range dv.unified.Hunks {
  223. dv.beforeNumDigits = max(dv.beforeNumDigits, len(strconv.Itoa(h.FromLine+len(h.Lines))))
  224. dv.afterNumDigits = max(dv.afterNumDigits, len(strconv.Itoa(h.ToLine+len(h.Lines))))
  225. }
  226. }
  227. func (dv *DiffView) detectTotalLines() {
  228. dv.totalLines = 0
  229. switch dv.layout {
  230. case layoutUnified:
  231. for _, h := range dv.unified.Hunks {
  232. dv.totalLines += 1 + len(h.Lines)
  233. }
  234. case layoutSplit:
  235. for _, h := range dv.splitHunks {
  236. dv.totalLines += 1 + len(h.lines)
  237. }
  238. }
  239. }
  240. func (dv *DiffView) preventInfiniteYScroll() {
  241. if dv.infiniteYScroll {
  242. return
  243. }
  244. // clamp yOffset to prevent scrolling beyond the last line
  245. if dv.height > 0 {
  246. maxYOffset := max(0, dv.totalLines-dv.height)
  247. dv.yOffset = min(dv.yOffset, maxYOffset)
  248. } else {
  249. // if no height limit, ensure yOffset doesn't exceed total lines
  250. dv.yOffset = min(dv.yOffset, max(0, dv.totalLines-1))
  251. }
  252. dv.yOffset = max(0, dv.yOffset) // ensure yOffset is not negative
  253. }
  254. // detectCodeWidth calculates the maximum width of code lines in the diff view.
  255. func (dv *DiffView) detectCodeWidth() {
  256. switch dv.layout {
  257. case layoutUnified:
  258. dv.detectUnifiedCodeWidth()
  259. case layoutSplit:
  260. dv.detectSplitCodeWidth()
  261. }
  262. dv.fullCodeWidth = dv.codeWidth + leadingSymbolsSize
  263. }
  264. // detectUnifiedCodeWidth calculates the maximum width of code lines in a
  265. // unified diff.
  266. func (dv *DiffView) detectUnifiedCodeWidth() {
  267. dv.codeWidth = 0
  268. for _, h := range dv.unified.Hunks {
  269. shownLines := ansi.StringWidth(dv.hunkLineFor(h))
  270. for _, l := range h.Lines {
  271. lineWidth := ansi.StringWidth(strings.TrimSuffix(l.Content, "\n")) + 1
  272. dv.codeWidth = max(dv.codeWidth, lineWidth, shownLines)
  273. }
  274. }
  275. }
  276. // detectSplitCodeWidth calculates the maximum width of code lines in a
  277. // split diff.
  278. func (dv *DiffView) detectSplitCodeWidth() {
  279. dv.codeWidth = 0
  280. for i, h := range dv.splitHunks {
  281. shownLines := ansi.StringWidth(dv.hunkLineFor(dv.unified.Hunks[i]))
  282. for _, l := range h.lines {
  283. if l.before != nil {
  284. codeWidth := ansi.StringWidth(strings.TrimSuffix(l.before.Content, "\n")) + 1
  285. dv.codeWidth = max(dv.codeWidth, codeWidth, shownLines)
  286. }
  287. if l.after != nil {
  288. codeWidth := ansi.StringWidth(strings.TrimSuffix(l.after.Content, "\n")) + 1
  289. dv.codeWidth = max(dv.codeWidth, codeWidth, shownLines)
  290. }
  291. }
  292. }
  293. }
  294. // resizeCodeWidth resizes the code width to fit within the specified width.
  295. func (dv *DiffView) resizeCodeWidth() {
  296. fullNumWidth := dv.beforeNumDigits + dv.afterNumDigits
  297. fullNumWidth += lineNumPadding * 4 // left and right padding for both line numbers
  298. switch dv.layout {
  299. case layoutUnified:
  300. dv.codeWidth = dv.width - fullNumWidth - leadingSymbolsSize
  301. case layoutSplit:
  302. remainingWidth := dv.width - fullNumWidth - leadingSymbolsSize*2
  303. dv.codeWidth = remainingWidth / 2
  304. dv.extraColOnAfter = isOdd(remainingWidth)
  305. }
  306. dv.fullCodeWidth = dv.codeWidth + leadingSymbolsSize
  307. }
  308. // renderUnified renders the unified diff view as a string.
  309. func (dv *DiffView) renderUnified() string {
  310. var b strings.Builder
  311. fullContentStyle := lipgloss.NewStyle().MaxWidth(dv.fullCodeWidth)
  312. printedLines := -dv.yOffset
  313. shouldWrite := func() bool { return printedLines >= 0 }
  314. getContent := func(in string, ls LineStyle) (content string, leadingEllipsis bool) {
  315. content = strings.TrimSuffix(in, "\n")
  316. content = dv.hightlightCode(content, ls.Code.GetBackground())
  317. content = ansi.GraphemeWidth.Cut(content, dv.xOffset, len(content))
  318. content = ansi.Truncate(content, dv.codeWidth, "…")
  319. leadingEllipsis = dv.xOffset > 0 && strings.TrimSpace(content) != ""
  320. return
  321. }
  322. outer:
  323. for i, h := range dv.unified.Hunks {
  324. if shouldWrite() {
  325. ls := dv.style.DividerLine
  326. if dv.lineNumbers {
  327. b.WriteString(ls.LineNumber.Render(pad("…", dv.beforeNumDigits)))
  328. b.WriteString(ls.LineNumber.Render(pad("…", dv.afterNumDigits)))
  329. }
  330. content := ansi.Truncate(dv.hunkLineFor(h), dv.fullCodeWidth, "…")
  331. b.WriteString(ls.Code.Width(dv.fullCodeWidth).Render(content))
  332. b.WriteString("\n")
  333. }
  334. printedLines++
  335. beforeLine := h.FromLine
  336. afterLine := h.ToLine
  337. for j, l := range h.Lines {
  338. // print ellipis if we don't have enough space to print the rest of the diff
  339. hasReachedHeight := dv.height > 0 && printedLines+1 == dv.height
  340. isLastHunk := i+1 == len(dv.unified.Hunks)
  341. isLastLine := j+1 == len(h.Lines)
  342. if hasReachedHeight && (!isLastHunk || !isLastLine) {
  343. if shouldWrite() {
  344. ls := dv.lineStyleForType(l.Kind)
  345. if dv.lineNumbers {
  346. b.WriteString(ls.LineNumber.Render(pad("…", dv.beforeNumDigits)))
  347. b.WriteString(ls.LineNumber.Render(pad("…", dv.afterNumDigits)))
  348. }
  349. b.WriteString(fullContentStyle.Render(
  350. ls.Code.Width(dv.fullCodeWidth).Render(" …"),
  351. ))
  352. b.WriteRune('\n')
  353. }
  354. break outer
  355. }
  356. switch l.Kind {
  357. case udiff.Equal:
  358. if shouldWrite() {
  359. ls := dv.style.EqualLine
  360. content, leadingEllipsis := getContent(l.Content, ls)
  361. if dv.lineNumbers {
  362. b.WriteString(ls.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
  363. b.WriteString(ls.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
  364. }
  365. b.WriteString(fullContentStyle.Render(
  366. ls.Code.Width(dv.fullCodeWidth).Render(ternary(leadingEllipsis, " …", " ") + content),
  367. ))
  368. }
  369. beforeLine++
  370. afterLine++
  371. case udiff.Insert:
  372. if shouldWrite() {
  373. ls := dv.style.InsertLine
  374. content, leadingEllipsis := getContent(l.Content, ls)
  375. if dv.lineNumbers {
  376. b.WriteString(ls.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
  377. b.WriteString(ls.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
  378. }
  379. b.WriteString(fullContentStyle.Render(
  380. ls.Symbol.Render(ternary(leadingEllipsis, "+…", "+ ")) +
  381. ls.Code.Width(dv.codeWidth).Render(content),
  382. ))
  383. }
  384. afterLine++
  385. case udiff.Delete:
  386. if shouldWrite() {
  387. ls := dv.style.DeleteLine
  388. content, leadingEllipsis := getContent(l.Content, ls)
  389. if dv.lineNumbers {
  390. b.WriteString(ls.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
  391. b.WriteString(ls.LineNumber.Render(pad(" ", dv.afterNumDigits)))
  392. }
  393. b.WriteString(fullContentStyle.Render(
  394. ls.Symbol.Render(ternary(leadingEllipsis, "-…", "- ")) +
  395. ls.Code.Width(dv.codeWidth).Render(content),
  396. ))
  397. }
  398. beforeLine++
  399. }
  400. if shouldWrite() {
  401. b.WriteRune('\n')
  402. }
  403. printedLines++
  404. }
  405. }
  406. for printedLines < dv.height {
  407. if shouldWrite() {
  408. ls := dv.style.MissingLine
  409. if dv.lineNumbers {
  410. b.WriteString(ls.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
  411. b.WriteString(ls.LineNumber.Render(pad(" ", dv.afterNumDigits)))
  412. }
  413. b.WriteString(ls.Code.Width(dv.fullCodeWidth).Render(" "))
  414. b.WriteRune('\n')
  415. }
  416. printedLines++
  417. }
  418. return b.String()
  419. }
  420. // renderSplit renders the split (side-by-side) diff view as a string.
  421. func (dv *DiffView) renderSplit() string {
  422. var b strings.Builder
  423. beforeFullContentStyle := lipgloss.NewStyle().MaxWidth(dv.fullCodeWidth)
  424. afterFullContentStyle := lipgloss.NewStyle().MaxWidth(dv.fullCodeWidth + btoi(dv.extraColOnAfter))
  425. printedLines := -dv.yOffset
  426. shouldWrite := func() bool { return printedLines >= 0 }
  427. getContent := func(in string, ls LineStyle) (content string, leadingEllipsis bool) {
  428. content = strings.TrimSuffix(in, "\n")
  429. content = dv.hightlightCode(content, ls.Code.GetBackground())
  430. content = ansi.GraphemeWidth.Cut(content, dv.xOffset, len(content))
  431. content = ansi.Truncate(content, dv.codeWidth, "…")
  432. leadingEllipsis = dv.xOffset > 0 && strings.TrimSpace(content) != ""
  433. return
  434. }
  435. outer:
  436. for i, h := range dv.splitHunks {
  437. if shouldWrite() {
  438. ls := dv.style.DividerLine
  439. if dv.lineNumbers {
  440. b.WriteString(ls.LineNumber.Render(pad("…", dv.beforeNumDigits)))
  441. }
  442. content := ansi.Truncate(dv.hunkLineFor(dv.unified.Hunks[i]), dv.fullCodeWidth, "…")
  443. b.WriteString(ls.Code.Width(dv.fullCodeWidth).Render(content))
  444. if dv.lineNumbers {
  445. b.WriteString(ls.LineNumber.Render(pad("…", dv.afterNumDigits)))
  446. }
  447. b.WriteString(ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "))
  448. b.WriteRune('\n')
  449. }
  450. printedLines++
  451. beforeLine := h.fromLine
  452. afterLine := h.toLine
  453. for j, l := range h.lines {
  454. // print ellipis if we don't have enough space to print the rest of the diff
  455. hasReachedHeight := dv.height > 0 && printedLines+1 == dv.height
  456. isLastHunk := i+1 == len(dv.unified.Hunks)
  457. isLastLine := j+1 == len(h.lines)
  458. if hasReachedHeight && (!isLastHunk || !isLastLine) {
  459. if shouldWrite() {
  460. ls := dv.style.MissingLine
  461. if l.before != nil {
  462. ls = dv.lineStyleForType(l.before.Kind)
  463. }
  464. if dv.lineNumbers {
  465. b.WriteString(ls.LineNumber.Render(pad("…", dv.beforeNumDigits)))
  466. }
  467. b.WriteString(beforeFullContentStyle.Render(
  468. ls.Code.Width(dv.fullCodeWidth).Render(" …"),
  469. ))
  470. ls = dv.style.MissingLine
  471. if l.after != nil {
  472. ls = dv.lineStyleForType(l.after.Kind)
  473. }
  474. if dv.lineNumbers {
  475. b.WriteString(ls.LineNumber.Render(pad("…", dv.afterNumDigits)))
  476. }
  477. b.WriteString(afterFullContentStyle.Render(
  478. ls.Code.Width(dv.fullCodeWidth).Render(" …"),
  479. ))
  480. b.WriteRune('\n')
  481. }
  482. break outer
  483. }
  484. switch {
  485. case l.before == nil:
  486. if shouldWrite() {
  487. ls := dv.style.MissingLine
  488. if dv.lineNumbers {
  489. b.WriteString(ls.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
  490. }
  491. b.WriteString(beforeFullContentStyle.Render(
  492. ls.Code.Width(dv.fullCodeWidth).Render(" "),
  493. ))
  494. }
  495. case l.before.Kind == udiff.Equal:
  496. if shouldWrite() {
  497. ls := dv.style.EqualLine
  498. content, leadingEllipsis := getContent(l.before.Content, ls)
  499. if dv.lineNumbers {
  500. b.WriteString(ls.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
  501. }
  502. b.WriteString(beforeFullContentStyle.Render(
  503. ls.Code.Width(dv.fullCodeWidth).Render(ternary(leadingEllipsis, " …", " ") + content),
  504. ))
  505. }
  506. beforeLine++
  507. case l.before.Kind == udiff.Delete:
  508. if shouldWrite() {
  509. ls := dv.style.DeleteLine
  510. content, leadingEllipsis := getContent(l.before.Content, ls)
  511. if dv.lineNumbers {
  512. b.WriteString(ls.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
  513. }
  514. b.WriteString(beforeFullContentStyle.Render(
  515. ls.Symbol.Render(ternary(leadingEllipsis, "-…", "- ")) +
  516. ls.Code.Width(dv.codeWidth).Render(content),
  517. ))
  518. }
  519. beforeLine++
  520. }
  521. switch {
  522. case l.after == nil:
  523. if shouldWrite() {
  524. ls := dv.style.MissingLine
  525. if dv.lineNumbers {
  526. b.WriteString(ls.LineNumber.Render(pad(" ", dv.afterNumDigits)))
  527. }
  528. b.WriteString(afterFullContentStyle.Render(
  529. ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "),
  530. ))
  531. }
  532. case l.after.Kind == udiff.Equal:
  533. if shouldWrite() {
  534. ls := dv.style.EqualLine
  535. content, leadingEllipsis := getContent(l.after.Content, ls)
  536. if dv.lineNumbers {
  537. b.WriteString(ls.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
  538. }
  539. b.WriteString(afterFullContentStyle.Render(
  540. ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(ternary(leadingEllipsis, " …", " ") + content),
  541. ))
  542. }
  543. afterLine++
  544. case l.after.Kind == udiff.Insert:
  545. if shouldWrite() {
  546. ls := dv.style.InsertLine
  547. content, leadingEllipsis := getContent(l.after.Content, ls)
  548. if dv.lineNumbers {
  549. b.WriteString(ls.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
  550. }
  551. b.WriteString(afterFullContentStyle.Render(
  552. ls.Symbol.Render(ternary(leadingEllipsis, "+…", "+ ")) +
  553. ls.Code.Width(dv.codeWidth+btoi(dv.extraColOnAfter)).Render(content),
  554. ))
  555. }
  556. afterLine++
  557. }
  558. if shouldWrite() {
  559. b.WriteRune('\n')
  560. }
  561. printedLines++
  562. }
  563. }
  564. for printedLines < dv.height {
  565. if shouldWrite() {
  566. ls := dv.style.MissingLine
  567. if dv.lineNumbers {
  568. b.WriteString(ls.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
  569. }
  570. b.WriteString(ls.Code.Width(dv.fullCodeWidth).Render(" "))
  571. if dv.lineNumbers {
  572. b.WriteString(ls.LineNumber.Render(pad(" ", dv.afterNumDigits)))
  573. }
  574. b.WriteString(ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "))
  575. b.WriteRune('\n')
  576. }
  577. printedLines++
  578. }
  579. return b.String()
  580. }
  581. // hunkLineFor formats the header line for a hunk in the unified diff view.
  582. func (dv *DiffView) hunkLineFor(h *udiff.Hunk) string {
  583. beforeShownLines, afterShownLines := dv.hunkShownLines(h)
  584. return fmt.Sprintf(
  585. " @@ -%d,%d +%d,%d @@ ",
  586. h.FromLine,
  587. beforeShownLines,
  588. h.ToLine,
  589. afterShownLines,
  590. )
  591. }
  592. // hunkShownLines calculates the number of lines shown in a hunk for both before
  593. // and after versions.
  594. func (dv *DiffView) hunkShownLines(h *udiff.Hunk) (before, after int) {
  595. for _, l := range h.Lines {
  596. switch l.Kind {
  597. case udiff.Equal:
  598. before++
  599. after++
  600. case udiff.Insert:
  601. after++
  602. case udiff.Delete:
  603. before++
  604. }
  605. }
  606. return
  607. }
  608. func (dv *DiffView) lineStyleForType(t udiff.OpKind) LineStyle {
  609. switch t {
  610. case udiff.Equal:
  611. return dv.style.EqualLine
  612. case udiff.Insert:
  613. return dv.style.InsertLine
  614. case udiff.Delete:
  615. return dv.style.DeleteLine
  616. default:
  617. return dv.style.MissingLine
  618. }
  619. }
  620. func (dv *DiffView) hightlightCode(source string, bgColor color.Color) string {
  621. if dv.chromaStyle == nil {
  622. return source
  623. }
  624. l := dv.getChromaLexer(source)
  625. f := dv.getChromaFormatter(bgColor)
  626. it, err := l.Tokenise(nil, source)
  627. if err != nil {
  628. return source
  629. }
  630. var b strings.Builder
  631. if err := f.Format(&b, dv.chromaStyle, it); err != nil {
  632. return source
  633. }
  634. return b.String()
  635. }
  636. func (dv *DiffView) getChromaLexer(source string) chroma.Lexer {
  637. l := lexers.Match(dv.before.path)
  638. if l == nil {
  639. l = lexers.Analyse(source)
  640. }
  641. if l == nil {
  642. l = lexers.Fallback
  643. }
  644. return chroma.Coalesce(l)
  645. }
  646. func (dv *DiffView) getChromaFormatter(gbColor color.Color) chroma.Formatter {
  647. return chromaFormatter{
  648. bgColor: gbColor,
  649. }
  650. }