267 lines
9.2 KiB
Odin
267 lines
9.2 KiB
Odin
package main
|
|
|
|
import "core:strings"
|
|
|
|
// Side-by-side diff viewer. A unified diff is parsed into aligned rows:
|
|
// context lines appear on both sides, removed/added runs are paired up
|
|
// line-by-line (with blank filler cells for the unmatched remainder), and
|
|
// hunk headers become full-width separator rows. A buffer whose `diff`
|
|
// field is set renders this view instead of its text.
|
|
|
|
Diff_Row :: struct {
|
|
header: bool,
|
|
header_text: string,
|
|
left_number: int, // 0 = no line on this side
|
|
right_number: int,
|
|
left_text: string,
|
|
right_text: string,
|
|
left_kind: u8, // '-' removed, ' ' context, 0 filler
|
|
right_kind: u8, // '+' added, ' ' context, 0 filler
|
|
}
|
|
|
|
Diff_Document :: struct {
|
|
rows: [dynamic]Diff_Row,
|
|
scroll: int,
|
|
}
|
|
|
|
diff_document_destroy :: proc(doc: ^Diff_Document) {
|
|
for row in doc.rows {
|
|
delete(row.header_text)
|
|
delete(row.left_text)
|
|
delete(row.right_text)
|
|
}
|
|
delete(doc.rows)
|
|
free(doc)
|
|
}
|
|
|
|
diff_parse_leading_int :: proc(s: string) -> int {
|
|
value := 0
|
|
for b in transmute([]u8)s {
|
|
if b < '0' || b > '9' do break
|
|
value = value * 10 + int(b - '0')
|
|
}
|
|
return value
|
|
}
|
|
|
|
// "@@ -12,5 +14,6 @@ ..." -> (12, 14)
|
|
diff_parse_hunk_header :: proc(line: string) -> (int, int) {
|
|
left, right := 1, 1
|
|
if minus := strings.index_byte(line, '-'); minus >= 0 {
|
|
left = max_int(diff_parse_leading_int(line[minus + 1:]), 1)
|
|
}
|
|
if plus := strings.index_byte(line, '+'); plus >= 0 {
|
|
right = max_int(diff_parse_leading_int(line[plus + 1:]), 1)
|
|
}
|
|
return left, right
|
|
}
|
|
|
|
// Pairs the collected removed/added runs into aligned rows.
|
|
diff_flush_changes :: proc(doc: ^Diff_Document, removed, added: ^[dynamic]string, left_num, right_num: ^int) {
|
|
count := max_int(len(removed), len(added))
|
|
for i in 0 ..< count {
|
|
row := Diff_Row{}
|
|
if i < len(removed) {
|
|
row.left_text = strings.clone(removed[i])
|
|
row.left_kind = '-'
|
|
row.left_number = left_num^
|
|
left_num^ += 1
|
|
}
|
|
if i < len(added) {
|
|
row.right_text = strings.clone(added[i])
|
|
row.right_kind = '+'
|
|
row.right_number = right_num^
|
|
right_num^ += 1
|
|
}
|
|
append(&doc.rows, row)
|
|
}
|
|
clear(removed)
|
|
clear(added)
|
|
}
|
|
|
|
diff_document_make :: proc(text: string) -> ^Diff_Document {
|
|
doc := new(Diff_Document)
|
|
removed := make([dynamic]string, context.temp_allocator)
|
|
added := make([dynamic]string, context.temp_allocator)
|
|
left_num, right_num := 1, 1
|
|
in_hunk := false
|
|
|
|
lines, _ := strings.split_lines(text, context.temp_allocator)
|
|
for raw_line in lines {
|
|
line := strings.trim_right(raw_line, "\r")
|
|
|
|
if strings.has_prefix(line, "@@") {
|
|
diff_flush_changes(doc, &removed, &added, &left_num, &right_num)
|
|
left_num, right_num = diff_parse_hunk_header(line)
|
|
in_hunk = true
|
|
append(&doc.rows, Diff_Row{header = true, header_text = strings.clone(line)})
|
|
continue
|
|
}
|
|
if strings.has_prefix(line, "diff --git") {
|
|
diff_flush_changes(doc, &removed, &added, &left_num, &right_num)
|
|
in_hunk = false
|
|
continue
|
|
}
|
|
if !in_hunk {
|
|
// File headers (index, ---, +++, mode changes, binary notes).
|
|
if len(strings.trim_space(line)) > 0 && len(doc.rows) == 0 && !strings.has_prefix(line, "index ") &&
|
|
!strings.has_prefix(line, "---") && !strings.has_prefix(line, "+++") &&
|
|
!strings.has_prefix(line, "old mode") && !strings.has_prefix(line, "new mode") &&
|
|
!strings.has_prefix(line, "new file") && !strings.has_prefix(line, "deleted file") &&
|
|
!strings.has_prefix(line, "similarity") && !strings.has_prefix(line, "rename") &&
|
|
!strings.has_prefix(line, "copy") {
|
|
// Notes like "Binary files differ" get a visible header row.
|
|
append(&doc.rows, Diff_Row{header = true, header_text = strings.clone(line)})
|
|
}
|
|
continue
|
|
}
|
|
if len(line) == 0 do continue
|
|
|
|
switch line[0] {
|
|
case ' ':
|
|
diff_flush_changes(doc, &removed, &added, &left_num, &right_num)
|
|
content := line[1:]
|
|
append(&doc.rows, Diff_Row{
|
|
left_number = left_num,
|
|
right_number = right_num,
|
|
left_text = strings.clone(content),
|
|
right_text = strings.clone(content),
|
|
left_kind = ' ',
|
|
right_kind = ' ',
|
|
})
|
|
left_num += 1
|
|
right_num += 1
|
|
case '-':
|
|
append(&removed, line[1:])
|
|
case '+':
|
|
append(&added, line[1:])
|
|
case '\\':
|
|
// "\ No newline at end of file"
|
|
}
|
|
}
|
|
diff_flush_changes(doc, &removed, &added, &left_num, &right_num)
|
|
return doc
|
|
}
|
|
|
|
// Opens (or refreshes) a diff tab for the given unified diff text.
|
|
editor_open_diff :: proc(editor: ^Editor, name, diff_text: string) {
|
|
doc := diff_document_make(diff_text)
|
|
|
|
for &buffer, index in editor.buffers {
|
|
if buffer.scratch && buffer.path == name {
|
|
editor_buffer_destroy(&buffer)
|
|
buffer = diff_editor_buffer(name, doc)
|
|
editor.active = index
|
|
return
|
|
}
|
|
}
|
|
append(&editor.buffers, diff_editor_buffer(name, doc))
|
|
editor.active = len(editor.buffers) - 1
|
|
}
|
|
|
|
diff_editor_buffer :: proc(name: string, doc: ^Diff_Document) -> Editor_Buffer {
|
|
return Editor_Buffer{
|
|
path = strings.clone(name),
|
|
daemon_path = strings.clone(""),
|
|
buffer = buffer_make(""),
|
|
scratch = true,
|
|
diff = doc,
|
|
}
|
|
}
|
|
|
|
diff_view_scroll :: proc(active: ^Editor_Buffer, view: ^SDL_View, delta: int) {
|
|
doc := active.diff
|
|
visible := visible_editor_lines(view)
|
|
doc.scroll = clamp_int(doc.scroll + delta, 0, max_int(len(doc.rows) - visible, 0))
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Rendering
|
|
|
|
DIFF_REMOVED_BG :: [3]u8{58, 33, 36}
|
|
DIFF_ADDED_BG :: [3]u8{34, 51, 38}
|
|
DIFF_FILLER_BG :: [3]u8{33, 34, 39}
|
|
DIFF_HEADER_BG :: [3]u8{39, 41, 49}
|
|
|
|
render_diff_view_gpu :: proc(gpu: ^GPU_Renderer, view: ^SDL_View, active: ^Editor_Buffer, tasks_panel: ^Gradle_Tasks_Panel) {
|
|
doc := active.diff
|
|
sidebar_width := left_sidebar_width(view)
|
|
right_limit := content_right_edge(gpu.width) - 8
|
|
if tasks_panel != nil && tasks_panel.open {
|
|
right_limit = content_right_edge(gpu.width) - right_sidebar_width_view(view, gpu.width) - 8
|
|
}
|
|
|
|
area_x := f32(sidebar_width)
|
|
area_width := f32(right_limit) - area_x
|
|
area_bottom := f32(editor_content_bottom(view))
|
|
half := area_width * 0.5
|
|
advance := max_f32(gpu.font_advance, 1)
|
|
gutter_width := 5 * advance + 10
|
|
columns := max_int(int((half - gutter_width - 16) / advance), 4)
|
|
|
|
visible := visible_editor_lines(view)
|
|
doc.scroll = clamp_int(doc.scroll, 0, max_int(len(doc.rows) - visible, 0))
|
|
|
|
y := f32(SDL_EDITOR_TEXT_Y)
|
|
for offset in 0 ..< visible {
|
|
index := doc.scroll + offset
|
|
if index >= len(doc.rows) do break
|
|
row := doc.rows[index]
|
|
|
|
if row.header {
|
|
gpu_rect(gpu, area_x, y - 3, area_width, SDL_LINE_HEIGHT + 1, DIFF_HEADER_BG[0], DIFF_HEADER_BG[1], DIFF_HEADER_BG[2], 255)
|
|
gpu_text_limited(gpu, area_x + 12, y, row.header_text, max_int(int((area_width - 24) / advance), 4), 150, 160, 180, 255)
|
|
} else {
|
|
render_diff_side_gpu(gpu, area_x, y, half, gutter_width, columns, row.left_number, row.left_text, row.left_kind)
|
|
render_diff_side_gpu(gpu, area_x + half + 1, y, half - 1, gutter_width, columns, row.right_number, row.right_text, row.right_kind)
|
|
}
|
|
y += SDL_LINE_HEIGHT
|
|
}
|
|
|
|
// Center divider on top of the row backgrounds.
|
|
gpu_rect(gpu, area_x + half, SDL_EDITOR_TOP, 1, area_bottom - SDL_EDITOR_TOP, 52, 54, 62, 255)
|
|
}
|
|
|
|
render_diff_side_gpu :: proc(gpu: ^GPU_Renderer, x, y, width, gutter_width: f32, columns, number: int, text: string, kind: u8) {
|
|
background: [3]u8
|
|
has_background := true
|
|
switch kind {
|
|
case '-':
|
|
background = DIFF_REMOVED_BG
|
|
case '+':
|
|
background = DIFF_ADDED_BG
|
|
case 0:
|
|
background = DIFF_FILLER_BG
|
|
case:
|
|
has_background = false
|
|
}
|
|
if has_background {
|
|
gpu_rect(gpu, x, y - 3, width, SDL_LINE_HEIGHT + 1, background[0], background[1], background[2], 255)
|
|
}
|
|
if kind == 0 do return
|
|
|
|
if number > 0 {
|
|
gpu_text(gpu, x + 6, y, fmt_diff_number(number), 116, 120, 128, 255)
|
|
}
|
|
marker_color := [3]u8{210, 214, 222}
|
|
if kind == '-' do marker_color = {235, 140, 145}
|
|
if kind == '+' do marker_color = {160, 210, 140}
|
|
gpu_text_limited(gpu, x + gutter_width, y, text, columns, marker_color[0], marker_color[1], marker_color[2], 255)
|
|
}
|
|
|
|
fmt_diff_number :: proc(number: int) -> string {
|
|
buf: [8]u8
|
|
value := number
|
|
index := len(buf)
|
|
for value > 0 && index > 0 {
|
|
index -= 1
|
|
buf[index] = '0' + u8(value % 10)
|
|
value /= 10
|
|
}
|
|
out := make([]u8, 4, context.temp_allocator)
|
|
for i in 0 ..< 4 {
|
|
out[i] = ' '
|
|
}
|
|
digits := len(buf) - index
|
|
copy(out[max_int(4 - digits, 0):], buf[index:])
|
|
return string(out)
|
|
}
|