Dont store full rendered lines, instead fill them up at actual draw time. Makes implementing mouse selection and searching more robust.
746 lines
23 KiB
Go
746 lines
23 KiB
Go
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
|
|
|
|
package diff
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"kitty/tools/tui/graphics"
|
|
"kitty/tools/tui/loop"
|
|
"kitty/tools/tui/sgr"
|
|
"kitty/tools/utils"
|
|
"kitty/tools/utils/style"
|
|
"kitty/tools/wcswidth"
|
|
)
|
|
|
|
var _ = fmt.Print
|
|
|
|
type LineType int
|
|
|
|
const (
|
|
TITLE_LINE LineType = iota
|
|
CHANGE_LINE
|
|
CONTEXT_LINE
|
|
HUNK_TITLE_LINE
|
|
IMAGE_LINE
|
|
EMPTY_LINE
|
|
)
|
|
|
|
type Reference struct {
|
|
path string
|
|
linenum int // 1 based
|
|
}
|
|
|
|
type HalfScreenLine struct {
|
|
marked_up_margin_text string
|
|
marked_up_text string
|
|
is_filler bool
|
|
}
|
|
|
|
type ScreenLine struct {
|
|
left, right HalfScreenLine
|
|
}
|
|
|
|
type LogicalLine struct {
|
|
line_type LineType
|
|
screen_lines []*ScreenLine
|
|
is_full_width bool
|
|
is_change_start bool
|
|
left_reference, right_reference Reference
|
|
left_image, right_image struct {
|
|
key string
|
|
count int
|
|
}
|
|
image_lines_offset int
|
|
}
|
|
|
|
func (self *LogicalLine) render_screen_line(n int, lp *loop.Loop, margin_size, columns int) {
|
|
if n >= len(self.screen_lines) {
|
|
return
|
|
}
|
|
sl := self.screen_lines[n]
|
|
available_cols := columns/2 - margin_size
|
|
if self.is_full_width {
|
|
available_cols = columns - margin_size
|
|
}
|
|
left_margin := place_in(sl.left.marked_up_margin_text, margin_size)
|
|
left_text := place_in(sl.left.marked_up_text, available_cols)
|
|
if sl.left.is_filler {
|
|
left_margin = format_as_sgr.margin_filler + left_margin
|
|
left_text = format_as_sgr.filler + left_text
|
|
} else {
|
|
switch self.line_type {
|
|
case CHANGE_LINE:
|
|
left_margin = format_as_sgr.removed_margin + left_margin
|
|
left_text = format_as_sgr.removed + left_text
|
|
case HUNK_TITLE_LINE:
|
|
left_margin = format_as_sgr.hunk_margin + left_margin
|
|
left_text = format_as_sgr.hunk + left_text
|
|
case TITLE_LINE:
|
|
default:
|
|
left_margin = format_as_sgr.margin + left_margin
|
|
}
|
|
}
|
|
lp.QueueWriteString(left_margin + "\x1b[m")
|
|
lp.QueueWriteString(left_text)
|
|
if self.is_full_width {
|
|
return
|
|
}
|
|
right_margin := place_in(sl.right.marked_up_margin_text, margin_size)
|
|
right_text := place_in(sl.right.marked_up_text, available_cols)
|
|
if sl.right.is_filler {
|
|
right_margin = format_as_sgr.margin_filler + right_margin
|
|
right_text = format_as_sgr.filler + right_text
|
|
} else {
|
|
switch self.line_type {
|
|
case CHANGE_LINE:
|
|
right_margin = format_as_sgr.added_margin + right_margin
|
|
right_text = format_as_sgr.added + right_text
|
|
case HUNK_TITLE_LINE:
|
|
right_margin = format_as_sgr.hunk_margin + right_margin
|
|
right_text = format_as_sgr.hunk + right_text
|
|
case TITLE_LINE:
|
|
default:
|
|
right_margin = format_as_sgr.margin + right_margin
|
|
}
|
|
}
|
|
lp.QueueWriteString("\x1b[m\r")
|
|
lp.MoveCursorHorizontally(available_cols + margin_size)
|
|
lp.QueueWriteString(right_margin + "\x1b[m")
|
|
lp.QueueWriteString(right_text)
|
|
}
|
|
|
|
func (self *LogicalLine) IncrementScrollPosBy(pos *ScrollPos, amt int) (delta int) {
|
|
if len(self.screen_lines) > 0 {
|
|
npos := utils.Max(0, utils.Min(pos.screen_line+amt, len(self.screen_lines)-1))
|
|
delta = npos - pos.screen_line
|
|
pos.screen_line = npos
|
|
}
|
|
return
|
|
}
|
|
|
|
func fit_in(text string, count int) string {
|
|
truncated := wcswidth.TruncateToVisualLength(text, count)
|
|
if len(truncated) >= len(text) {
|
|
return text
|
|
}
|
|
if count > 1 {
|
|
truncated = wcswidth.TruncateToVisualLength(text, count-1)
|
|
}
|
|
return truncated + `…`
|
|
}
|
|
|
|
func fill_in(text string, sz int) string {
|
|
w := wcswidth.Stringwidth(text)
|
|
if w < sz {
|
|
text += strings.Repeat(` `, (sz - w))
|
|
}
|
|
return text
|
|
}
|
|
|
|
func place_in(text string, sz int) string {
|
|
return fill_in(fit_in(text, sz), sz)
|
|
}
|
|
|
|
var format_as_sgr struct {
|
|
title, margin, added, removed, added_margin, removed_margin, filler, margin_filler, hunk_margin, hunk, selection string
|
|
}
|
|
|
|
var statusline_format, added_count_format, removed_count_format, message_format, selection_format func(...any) string
|
|
|
|
func create_formatters() {
|
|
ctx := style.Context{AllowEscapeCodes: true}
|
|
only_open := func(x string) string {
|
|
ans := ctx.SprintFunc(x)("|")
|
|
ans, _, _ = strings.Cut(ans, "|")
|
|
return ans
|
|
}
|
|
format_as_sgr.filler = only_open("bg=" + conf.Filler_bg.AsRGBSharp())
|
|
if conf.Margin_filler_bg.IsSet {
|
|
format_as_sgr.margin_filler = only_open("bg=" + conf.Margin_filler_bg.Color.AsRGBSharp())
|
|
} else {
|
|
format_as_sgr.margin_filler = only_open("bg=" + conf.Filler_bg.AsRGBSharp())
|
|
}
|
|
format_as_sgr.added = only_open("bg=" + conf.Added_bg.AsRGBSharp())
|
|
format_as_sgr.added_margin = only_open(fmt.Sprintf("fg=%s bg=%s", conf.Margin_fg.AsRGBSharp(), conf.Added_margin_bg.AsRGBSharp()))
|
|
format_as_sgr.removed = only_open("bg=" + conf.Removed_bg.AsRGBSharp())
|
|
format_as_sgr.removed_margin = only_open(fmt.Sprintf("fg=%s bg=%s", conf.Margin_fg.AsRGBSharp(), conf.Removed_margin_bg.AsRGBSharp()))
|
|
format_as_sgr.title = only_open(fmt.Sprintf("fg=%s bg=%s bold", conf.Title_fg.AsRGBSharp(), conf.Title_bg.AsRGBSharp()))
|
|
format_as_sgr.margin = only_open(fmt.Sprintf("fg=%s bg=%s", conf.Margin_fg.AsRGBSharp(), conf.Margin_bg.AsRGBSharp()))
|
|
format_as_sgr.hunk = only_open(fmt.Sprintf("fg=%s bg=%s", conf.Margin_fg.AsRGBSharp(), conf.Hunk_bg.AsRGBSharp()))
|
|
format_as_sgr.hunk_margin = only_open(fmt.Sprintf("fg=%s bg=%s", conf.Margin_fg.AsRGBSharp(), conf.Hunk_margin_bg.AsRGBSharp()))
|
|
statusline_format = ctx.SprintFunc(fmt.Sprintf("fg=%s", conf.Margin_fg.AsRGBSharp()))
|
|
added_count_format = ctx.SprintFunc(fmt.Sprintf("fg=%s", conf.Highlight_added_bg.AsRGBSharp()))
|
|
removed_count_format = ctx.SprintFunc(fmt.Sprintf("fg=%s", conf.Highlight_removed_bg.AsRGBSharp()))
|
|
message_format = ctx.SprintFunc("bold")
|
|
if conf.Select_fg.IsSet {
|
|
format_as_sgr.selection = only_open(fmt.Sprintf("fg=%s bg=%s", conf.Select_fg.Color.AsRGBSharp(), conf.Select_bg.AsRGBSharp()))
|
|
} else {
|
|
format_as_sgr.selection = only_open("bg=" + conf.Select_bg.AsRGBSharp())
|
|
}
|
|
}
|
|
|
|
func center_span(ltype string, offset, size int) *sgr.Span {
|
|
ans := sgr.NewSpan(offset, size)
|
|
switch ltype {
|
|
case "add":
|
|
ans.SetBackground(conf.Highlight_added_bg).SetClosingBackground(conf.Added_bg)
|
|
case "remove":
|
|
ans.SetBackground(conf.Highlight_removed_bg).SetClosingBackground(conf.Removed_bg)
|
|
}
|
|
return ans
|
|
}
|
|
|
|
func title_lines(left_path, right_path string, columns, margin_size int, ans []*LogicalLine) []*LogicalLine {
|
|
left_name, right_name := path_name_map[left_path], path_name_map[right_path]
|
|
available_cols := columns/2 - margin_size
|
|
ll := LogicalLine{
|
|
line_type: TITLE_LINE,
|
|
left_reference: Reference{path: left_path}, right_reference: Reference{path: right_path},
|
|
}
|
|
sl := ScreenLine{}
|
|
if right_name != "" && right_name != left_name {
|
|
sl.left.marked_up_text = format_as_sgr.title + fit_in(sanitize(left_name), available_cols)
|
|
sl.right.marked_up_text = format_as_sgr.title + fit_in(sanitize(right_name), available_cols)
|
|
} else {
|
|
sl.left.marked_up_text = format_as_sgr.title + fit_in(sanitize(left_name), columns-margin_size)
|
|
ll.is_full_width = true
|
|
}
|
|
l2 := ll
|
|
l2.line_type = EMPTY_LINE
|
|
ll.screen_lines = append(ll.screen_lines, &sl)
|
|
sl2 := ScreenLine{}
|
|
sl2.left.marked_up_margin_text = "\x1b[m" + strings.Repeat("━", margin_size)
|
|
sl2.left.marked_up_text = strings.Repeat("━", columns-margin_size)
|
|
l2.is_full_width = true
|
|
l2.screen_lines = append(l2.screen_lines, &sl2)
|
|
return append(ans, &ll, &l2)
|
|
}
|
|
|
|
type LogicalLines struct {
|
|
lines []*LogicalLine
|
|
margin_size, columns int
|
|
}
|
|
|
|
func (self *LogicalLines) At(i int) *LogicalLine { return self.lines[i] }
|
|
|
|
func (self *LogicalLines) ScreenLineAt(pos ScrollPos) *ScreenLine {
|
|
if pos.logical_line < len(self.lines) && pos.logical_line >= 0 {
|
|
line := self.lines[pos.logical_line]
|
|
if pos.screen_line < len(line.screen_lines) && pos.screen_line >= 0 {
|
|
return self.lines[pos.logical_line].screen_lines[pos.screen_line]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
func (self *LogicalLines) Len() int { return len(self.lines) }
|
|
|
|
func (self *LogicalLines) NumScreenLinesTo(a ScrollPos) (ans int) {
|
|
return self.Minus(a, ScrollPos{})
|
|
}
|
|
|
|
// a - b in terms of number of screen lines between the positions
|
|
func (self *LogicalLines) Minus(a, b ScrollPos) (delta int) {
|
|
if a.logical_line == b.logical_line {
|
|
return a.screen_line - b.screen_line
|
|
}
|
|
amt := 1
|
|
if a.Less(b) {
|
|
amt = -1
|
|
} else {
|
|
a, b = b, a
|
|
}
|
|
for i := a.logical_line; i < utils.Min(len(self.lines), b.logical_line+1); i++ {
|
|
line := self.lines[i]
|
|
switch i {
|
|
case a.logical_line:
|
|
delta += utils.Max(0, len(line.screen_lines)-a.screen_line)
|
|
case b.logical_line:
|
|
delta += b.screen_line
|
|
default:
|
|
delta += len(line.screen_lines)
|
|
}
|
|
}
|
|
return delta * amt
|
|
}
|
|
|
|
func (self *LogicalLines) IncrementScrollPosBy(pos *ScrollPos, amt int) (delta int) {
|
|
if pos.logical_line < 0 || pos.logical_line >= len(self.lines) || amt == 0 {
|
|
return
|
|
}
|
|
one := 1
|
|
if amt < 0 {
|
|
one = -1
|
|
}
|
|
for amt != 0 {
|
|
line := self.lines[pos.logical_line]
|
|
d := line.IncrementScrollPosBy(pos, amt)
|
|
if d == 0 {
|
|
nlp := pos.logical_line + one
|
|
if nlp < 0 || nlp >= len(self.lines) {
|
|
break
|
|
}
|
|
pos.logical_line = nlp
|
|
if one > 0 {
|
|
pos.screen_line = 0
|
|
} else {
|
|
pos.screen_line = len(self.lines[nlp].screen_lines) - 1
|
|
}
|
|
delta += one
|
|
amt -= one
|
|
} else {
|
|
amt -= d
|
|
delta += d
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func human_readable(size int64) string {
|
|
divisor, suffix := 1, "B"
|
|
for i, candidate := range []string{"B", "KB", "MB", "GB", "TB", "PB", "EB"} {
|
|
if size < (1 << ((i + 1) * 10)) {
|
|
divisor, suffix = (1 << (i * 10)), candidate
|
|
break
|
|
}
|
|
}
|
|
fs := float64(size) / float64(divisor)
|
|
s := strconv.FormatFloat(fs, 'f', 2, 64)
|
|
if idx := strings.Index(s, "."); idx > -1 {
|
|
s = s[:idx+2]
|
|
}
|
|
if strings.HasSuffix(s, ".0") || strings.HasSuffix(s, ".00") {
|
|
idx := strings.IndexByte(s, '.')
|
|
s = s[:idx]
|
|
}
|
|
return s + " " + suffix
|
|
}
|
|
|
|
func image_lines(left_path, right_path string, screen_size screen_size, margin_size int, image_size graphics.Size, ans []*LogicalLine) ([]*LogicalLine, error) {
|
|
columns := screen_size.columns
|
|
available_cols := columns/2 - margin_size
|
|
ll, err := first_binary_line(left_path, right_path, columns, margin_size, func(path string) (string, error) {
|
|
sz, err := size_for_path(path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
text := fmt.Sprintf("Size: %s", human_readable(sz))
|
|
res := image_collection.ResolutionOf(path)
|
|
if res.Width > -1 {
|
|
text = fmt.Sprintf("Dimensions: %dx%d %s", res.Width, res.Height, text)
|
|
}
|
|
return text, nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ll.image_lines_offset = len(ll.screen_lines)
|
|
|
|
do_side := func(path string) []string {
|
|
if path == "" {
|
|
return nil
|
|
}
|
|
sz, err := image_collection.GetSizeIfAvailable(path, image_size)
|
|
if err == nil {
|
|
count := int(math.Ceil(float64(sz.Height) / float64(screen_size.cell_height)))
|
|
return utils.Repeat("", count)
|
|
}
|
|
if errors.Is(err, graphics.ErrNotFound) {
|
|
return splitlines("Loading image...", available_cols)
|
|
}
|
|
return splitlines(fmt.Sprintf("Failed to load image: %s", err), available_cols)
|
|
}
|
|
left_lines := do_side(left_path)
|
|
if ll.left_image.count = len(left_lines); ll.left_image.count > 0 {
|
|
ll.left_image.key = left_path
|
|
}
|
|
right_lines := do_side(right_path)
|
|
if ll.right_image.count = len(right_lines); ll.right_image.count > 0 {
|
|
ll.right_image.key = right_path
|
|
}
|
|
for i := 0; i < utils.Max(len(left_lines), len(right_lines)); i++ {
|
|
sl := ScreenLine{}
|
|
if i < len(left_lines) {
|
|
sl.left.marked_up_text = left_lines[i]
|
|
}
|
|
if i < len(right_lines) {
|
|
sl.right.marked_up_text = right_lines[i]
|
|
}
|
|
ll.screen_lines = append(ll.screen_lines, &sl)
|
|
}
|
|
ll.line_type = IMAGE_LINE
|
|
return append(ans, ll), nil
|
|
}
|
|
|
|
type formatter = func(...any) string
|
|
|
|
func first_binary_line(left_path, right_path string, columns, margin_size int, renderer func(path string) (string, error)) (*LogicalLine, error) {
|
|
available_cols := columns/2 - margin_size
|
|
ll := LogicalLine{
|
|
is_change_start: true, line_type: CHANGE_LINE,
|
|
left_reference: Reference{path: left_path}, right_reference: Reference{path: right_path},
|
|
}
|
|
if left_path == "" {
|
|
line, err := renderer(right_path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, x := range splitlines(line, available_cols) {
|
|
sl := ScreenLine{}
|
|
sl.right.marked_up_text = x
|
|
ll.screen_lines = append(ll.screen_lines, &sl)
|
|
}
|
|
} else if right_path == "" {
|
|
line, err := renderer(left_path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, x := range splitlines(line, available_cols) {
|
|
sl := ScreenLine{}
|
|
sl.left.marked_up_text = x
|
|
ll.screen_lines = append(ll.screen_lines, &sl)
|
|
}
|
|
} else {
|
|
l, err := renderer(left_path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r, err := renderer(right_path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
left_lines, right_lines := splitlines(l, available_cols), splitlines(r, available_cols)
|
|
for i := 0; i < utils.Max(len(left_lines), len(right_lines)); i++ {
|
|
sl := ScreenLine{}
|
|
if i < len(left_lines) {
|
|
sl.left.marked_up_text = left_lines[i]
|
|
}
|
|
if i < len(right_lines) {
|
|
sl.right.marked_up_text = right_lines[i]
|
|
}
|
|
}
|
|
}
|
|
return &ll, nil
|
|
}
|
|
|
|
func binary_lines(left_path, right_path string, columns, margin_size int, ans []*LogicalLine) (ans2 []*LogicalLine, err error) {
|
|
ll, err := first_binary_line(left_path, right_path, columns, margin_size, func(path string) (string, error) {
|
|
sz, err := size_for_path(path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("Binary file: %s", human_readable(sz)), nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return append(ans, ll), nil
|
|
}
|
|
|
|
type DiffData struct {
|
|
left_path, right_path string
|
|
available_cols, margin_size int
|
|
|
|
left_lines, right_lines []string
|
|
}
|
|
|
|
func hunk_title(hunk *Hunk) string {
|
|
return fmt.Sprintf("@@ -%d,%d +%d,%d @@ %s", hunk.left_start+1, hunk.left_count, hunk.right_start+1, hunk.right_count, hunk.title)
|
|
}
|
|
|
|
func lines_for_context_chunk(data *DiffData, hunk_num int, chunk *Chunk, chunk_num int, ans []*LogicalLine) []*LogicalLine {
|
|
for i := 0; i < chunk.left_count; i++ {
|
|
left_line_number := chunk.left_start + i
|
|
right_line_number := chunk.right_start + i
|
|
ll := LogicalLine{line_type: CONTEXT_LINE,
|
|
left_reference: Reference{path: data.left_path, linenum: left_line_number + 1},
|
|
right_reference: Reference{path: data.right_path, linenum: right_line_number + 1},
|
|
}
|
|
left_line_number_s := strconv.Itoa(left_line_number + 1)
|
|
right_line_number_s := strconv.Itoa(right_line_number + 1)
|
|
for _, text := range splitlines(data.left_lines[left_line_number], data.available_cols) {
|
|
left_line := HalfScreenLine{left_line_number_s, text, false}
|
|
right_line := left_line
|
|
if right_line_number_s != left_line_number_s {
|
|
right_line = HalfScreenLine{right_line_number_s, text, false}
|
|
}
|
|
ll.screen_lines = append(ll.screen_lines, &ScreenLine{left_line, right_line})
|
|
left_line_number_s, right_line_number_s = "", ""
|
|
}
|
|
ans = append(ans, &ll)
|
|
}
|
|
return ans
|
|
}
|
|
|
|
func splitlines(text string, width int) []string {
|
|
return style.WrapTextAsLines(text, "", width)
|
|
}
|
|
|
|
func render_half_line(line_number int, line, ltype string, available_cols int, center Center, ans []HalfScreenLine) []HalfScreenLine {
|
|
size := center.left_size
|
|
if ltype != "remove" {
|
|
size = center.right_size
|
|
}
|
|
if size > 0 {
|
|
span := center_span(ltype, center.offset, size)
|
|
line = sgr.InsertFormatting(line, span)
|
|
}
|
|
lnum := strconv.Itoa(line_number + 1)
|
|
for _, sc := range splitlines(line, available_cols) {
|
|
ans = append(ans, HalfScreenLine{lnum, sc, false})
|
|
lnum = ""
|
|
}
|
|
return ans
|
|
}
|
|
|
|
func lines_for_diff_chunk(data *DiffData, hunk_num int, chunk *Chunk, chunk_num int, ans []*LogicalLine) []*LogicalLine {
|
|
common := utils.Min(chunk.left_count, chunk.right_count)
|
|
ll, rl := make([]HalfScreenLine, 0, 32), make([]HalfScreenLine, 0, 32)
|
|
for i := 0; i < utils.Max(chunk.left_count, chunk.right_count); i++ {
|
|
ll, rl = ll[:0], rl[:0]
|
|
var center Center
|
|
left_lnum, right_lnum := 0, 0
|
|
if i < len(chunk.centers) {
|
|
center = chunk.centers[i]
|
|
}
|
|
if i < chunk.left_count {
|
|
left_lnum = chunk.left_start + i
|
|
ll = render_half_line(left_lnum, data.left_lines[left_lnum], "remove", data.available_cols, center, ll)
|
|
left_lnum++
|
|
}
|
|
|
|
if i < chunk.right_count {
|
|
right_lnum = chunk.right_start + i
|
|
rl = render_half_line(right_lnum, data.right_lines[right_lnum], "add", data.available_cols, center, rl)
|
|
right_lnum++
|
|
}
|
|
|
|
if i < common {
|
|
extra := len(ll) - len(rl)
|
|
if extra < 0 {
|
|
ll = append(ll, utils.Repeat(HalfScreenLine{}, -extra)...)
|
|
} else if extra > 0 {
|
|
rl = append(rl, utils.Repeat(HalfScreenLine{}, extra)...)
|
|
}
|
|
} else {
|
|
if len(ll) > 0 {
|
|
rl = append(rl, utils.Repeat(HalfScreenLine{is_filler: true}, len(ll))...)
|
|
} else if len(rl) > 0 {
|
|
ll = append(ll, utils.Repeat(HalfScreenLine{is_filler: true}, len(rl))...)
|
|
}
|
|
}
|
|
logline := LogicalLine{
|
|
line_type: CHANGE_LINE, is_change_start: i == 0,
|
|
left_reference: Reference{path: data.left_path, linenum: left_lnum},
|
|
right_reference: Reference{path: data.left_path, linenum: right_lnum},
|
|
}
|
|
for l := 0; l < len(ll); l++ {
|
|
logline.screen_lines = append(logline.screen_lines, &ScreenLine{left: ll[l], right: rl[l]})
|
|
}
|
|
ans = append(ans, &logline)
|
|
}
|
|
return ans
|
|
}
|
|
|
|
func lines_for_diff(left_path string, right_path string, patch *Patch, columns, margin_size int, ans []*LogicalLine) (result []*LogicalLine, err error) {
|
|
ht := LogicalLine{
|
|
line_type: HUNK_TITLE_LINE,
|
|
left_reference: Reference{path: left_path}, right_reference: Reference{path: right_path},
|
|
is_full_width: true,
|
|
}
|
|
if patch.Len() == 0 {
|
|
for _, line := range splitlines("The files are identical", columns-margin_size) {
|
|
sl := ScreenLine{}
|
|
sl.left.marked_up_text = line
|
|
ht.screen_lines = append(ht.screen_lines, &sl)
|
|
}
|
|
ht.line_type = EMPTY_LINE
|
|
ht.is_full_width = true
|
|
return append(ans, &ht), nil
|
|
}
|
|
available_cols := columns/2 - margin_size
|
|
data := DiffData{left_path: left_path, right_path: right_path, available_cols: available_cols, margin_size: margin_size}
|
|
if left_path != "" {
|
|
data.left_lines, err = highlighted_lines_for_path(left_path)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
if right_path != "" {
|
|
data.right_lines, err = highlighted_lines_for_path(right_path)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
for hunk_num, hunk := range patch.all_hunks {
|
|
htl := ht
|
|
htl.left_reference.linenum = hunk.left_start + 1
|
|
htl.right_reference.linenum = hunk.right_start + 1
|
|
for _, line := range splitlines(hunk_title(hunk), columns-margin_size) {
|
|
sl := ScreenLine{}
|
|
sl.left.marked_up_text = line
|
|
htl.screen_lines = append(htl.screen_lines, &sl)
|
|
}
|
|
ans = append(ans, &htl)
|
|
for cnum, chunk := range hunk.chunks {
|
|
if chunk.is_context {
|
|
ans = lines_for_context_chunk(&data, hunk_num, chunk, cnum, ans)
|
|
} else {
|
|
ans = lines_for_diff_chunk(&data, hunk_num, chunk, cnum, ans)
|
|
}
|
|
}
|
|
}
|
|
return ans, nil
|
|
}
|
|
|
|
func all_lines(path string, columns, margin_size int, is_add bool, ans []*LogicalLine) ([]*LogicalLine, error) {
|
|
available_cols := columns/2 - margin_size
|
|
ltype := `add`
|
|
ll := LogicalLine{line_type: CHANGE_LINE}
|
|
if !is_add {
|
|
ltype = `remove`
|
|
ll.left_reference.path = path
|
|
} else {
|
|
ll.right_reference.path = path
|
|
}
|
|
lines, err := highlighted_lines_for_path(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var msg_lines []string
|
|
if is_add {
|
|
msg_lines = splitlines(`This file was added`, available_cols)
|
|
} else {
|
|
msg_lines = splitlines(`This file was removed`, available_cols)
|
|
}
|
|
for line_number, line := range lines {
|
|
hlines := make([]HalfScreenLine, 0, 8)
|
|
hlines = render_half_line(line_number, line, ltype, available_cols, Center{}, hlines)
|
|
l := ll
|
|
if is_add {
|
|
l.right_reference.linenum = line_number + 1
|
|
} else {
|
|
l.left_reference.linenum = line_number + 1
|
|
}
|
|
l.is_change_start = line_number == 0
|
|
for i, hl := range hlines {
|
|
sl := ScreenLine{}
|
|
if is_add {
|
|
sl.right = hl
|
|
if i < len(msg_lines) {
|
|
sl.left.marked_up_text = msg_lines[i]
|
|
} else {
|
|
sl.left.is_filler = true
|
|
}
|
|
} else {
|
|
sl.left = hl
|
|
if i < len(msg_lines) {
|
|
sl.right.marked_up_text = msg_lines[i]
|
|
} else {
|
|
sl.right.is_filler = true
|
|
}
|
|
}
|
|
l.screen_lines = append(l.screen_lines, &sl)
|
|
}
|
|
ans = append(ans, &l)
|
|
}
|
|
return ans, nil
|
|
}
|
|
|
|
func rename_lines(path, other_path string, columns, margin_size int, ans []*LogicalLine) ([]*LogicalLine, error) {
|
|
ll := LogicalLine{
|
|
left_reference: Reference{path: path}, right_reference: Reference{path: other_path},
|
|
line_type: CHANGE_LINE, is_change_start: true, is_full_width: true}
|
|
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) {
|
|
sl := ScreenLine{}
|
|
sl.right.marked_up_text = line
|
|
ll.screen_lines = append(ll.screen_lines, &sl)
|
|
}
|
|
return append(ans, &ll), nil
|
|
}
|
|
|
|
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) {
|
|
margin_size := utils.Max(3, len(strconv.Itoa(largest_line_number))+1)
|
|
ans := make([]*LogicalLine, 0, 1024)
|
|
empty_line := LogicalLine{line_type: EMPTY_LINE}
|
|
columns := screen_size.columns
|
|
err = collection.Apply(func(path, item_type, changed_path string) error {
|
|
ans = title_lines(path, changed_path, columns, margin_size, ans)
|
|
defer func() {
|
|
el := empty_line
|
|
ans = append(ans, &el)
|
|
}()
|
|
|
|
is_binary := !is_path_text(path)
|
|
if !is_binary && item_type == `diff` && !is_path_text(changed_path) {
|
|
is_binary = true
|
|
}
|
|
is_img := is_binary && is_image(path) || (item_type == `diff` && is_image(changed_path))
|
|
_ = is_img
|
|
switch item_type {
|
|
case "diff":
|
|
if is_binary {
|
|
if is_img {
|
|
ans, err = image_lines(path, changed_path, screen_size, margin_size, image_size, ans)
|
|
} else {
|
|
ans, err = binary_lines(path, changed_path, columns, margin_size, ans)
|
|
}
|
|
} else {
|
|
ans, err = lines_for_diff(path, changed_path, diff_map[path], columns, margin_size, ans)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case "add":
|
|
if is_binary {
|
|
if is_img {
|
|
ans, err = image_lines("", path, screen_size, margin_size, image_size, ans)
|
|
} else {
|
|
ans, err = binary_lines("", path, columns, margin_size, ans)
|
|
}
|
|
} else {
|
|
ans, err = all_lines(path, columns, margin_size, true, ans)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case "removal":
|
|
if is_binary {
|
|
if is_img {
|
|
ans, err = image_lines(path, "", screen_size, margin_size, image_size, ans)
|
|
} else {
|
|
ans, err = binary_lines(path, "", columns, margin_size, ans)
|
|
}
|
|
} else {
|
|
ans, err = all_lines(path, columns, margin_size, false, ans)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case "rename":
|
|
ans, err = rename_lines(path, changed_path, columns, margin_size, ans)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return fmt.Errorf("Unknown change type: %#v", item_type)
|
|
}
|
|
return nil
|
|
})
|
|
return &LogicalLines{lines: ans[:len(ans)-1], margin_size: margin_size, columns: columns}, err
|
|
}
|
|
|
|
func (self *LogicalLines) num_of_screen_lines() (ans int) {
|
|
for _, l := range self.lines {
|
|
ans += len(l.screen_lines)
|
|
}
|
|
return
|
|
}
|