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) }