This commit is contained in:
pavel 2026-07-08 10:28:32 +02:00
commit 5b65c4c0e4
11 changed files with 4237 additions and 297 deletions

267
client/odin/diff_view.odin Normal file
View file

@ -0,0 +1,267 @@
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)
}