;; Copyright (C) 2026 Tom Waddington ;; ;; This program is free software: you can redistribute it and/or modify ;; it under the terms of the GNU Affero General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;;; diff-split.scm - pairing a hunk's lines for side-by-side display ;;; ;;; A unified hunk lists deletions and additions one after another; a ;;; side-by-side view wants them beside each other. This turns one `hunk` into a ;;; list of two-sided rows: a context line fills both sides, a run of deletions ;;; is zipped index-wise with the run of additions that follows it, and whatever ;;; is left over in the longer run becomes one-sided rows. ;;; ;;; Both line numbers are threaded through, since each side counts in its own ;;; file: the old number advances on context and deletions, the new one on ;;; context and additions. ;;; ;;; Each side also carries the deletion's or addition's index within ;;; `hunk-lines`, which is what `build-apply-patch`'s include? predicate keys ;;; on, so a selected side-by-side row still resolves to exact patch lines. ;;; Context lines carry #f there: they are not stageable. ;;; ;;; Rows and sides are hashes rather than structs: they are display scaffolding, ;;; not model values, and the accessors below keep call sites readable. Pure. (require "model.scm") (provide split-hunk-rows split-row-kind split-row-left split-row-right split-side-line split-side-text split-side-index) ;;@doc ;; The side-by-side rows for `h`, in display order. Each row is a hash: ;; 'kind 'context | 'change | 'del | 'add | 'meta ;; 'left the old-file side, or #f when the row has none ;; 'right the new-file side, or #f ;; and each side is a hash of 'line (its number in that file, #f on a 'meta ;; row), 'text, and 'index (its position in `hunk-lines`, #f for context). (define (split-hunk-rows h) (walk-lines (hunk-lines h) (car (hunk-old-range h)) (car (hunk-new-range h)) 0 '())) (define (make-side line text index) (hash 'line line 'text text 'index index)) (define (make-split-row kind left right) (hash 'kind kind 'left left 'right right)) (define (split-row-kind r) (hash-ref r 'kind)) (define (split-row-left r) (hash-ref r 'left)) (define (split-row-right r) (hash-ref r 'right)) (define (split-side-line s) (hash-ref s 'line)) (define (split-side-text s) (hash-ref s 'text)) (define (split-side-index s) (hash-ref s 'index)) ;; `acc` accumulates rows reversed; `j` is the next index into `hunk-lines`. (define (walk-lines lines old new j acc) (cond [(null? lines) (reverse acc)] [(eq? (diff-line-kind (car lines)) 'context) (walk-lines (cdr lines) (+ old 1) (+ new 1) (+ j 1) (cons (context-row (car lines) old new) acc))] ;; "\ No newline at end of file" belongs to neither file and breaks any run ;; it lands in; git only ever emits it at the very end of a hunk. [(eq? (diff-line-kind (car lines)) 'meta) (walk-lines (cdr lines) old new (+ j 1) (cons (meta-row (car lines)) acc))] [else (walk-change lines old new j acc)])) ;; At a deletion or addition: take the run of deletions, then the run of ;; additions directly after it, and zip the two into rows. (define (walk-change lines old new j acc) (let* ([dels (take-run lines 'del j)] [adds (take-run (run-rest dels) 'add (run-next-index dels))] [rows (zip-runs (run-entries dels) (run-entries adds) old new '())]) (walk-lines (run-rest adds) (+ old (length (run-entries dels))) (+ new (length (run-entries adds))) (run-next-index adds) (prepend-reversed rows acc)))) ;; The leading run of `kind` lines: a hash of 'entries (a list of ;; (cons diff-line index) in order), 'rest (the lines after the run) and ;; 'next-index. (define (make-run entries rest next-index) (hash 'entries entries 'rest rest 'next-index next-index)) (define (run-entries r) (hash-ref r 'entries)) (define (run-rest r) (hash-ref r 'rest)) (define (run-next-index r) (hash-ref r 'next-index)) (define (take-run lines kind j) (collect-run lines kind j '())) (define (collect-run lines kind j acc) (if (and (pair? lines) (eq? (diff-line-kind (car lines)) kind)) (collect-run (cdr lines) kind (+ j 1) (cons (cons (car lines) j) acc)) (make-run (reverse acc) lines j))) ;; Pair deletions with additions position by position; the surplus of the ;; longer run becomes one-sided rows. (define (zip-runs dels adds old new acc) (cond [(and (null? dels) (null? adds)) (reverse acc)] [(null? adds) (zip-runs (cdr dels) adds (+ old 1) new (cons (del-row (car dels) old) acc))] [(null? dels) (zip-runs dels (cdr adds) old (+ new 1) (cons (add-row (car adds) new) acc))] [else (zip-runs (cdr dels) (cdr adds) (+ old 1) (+ new 1) (cons (change-row (car dels) (car adds) old new) acc))])) (define (prepend-reversed rows acc) (if (null? rows) acc (prepend-reversed (cdr rows) (cons (car rows) acc)))) (define (context-row dl old new) (make-split-row 'context (make-side old (diff-line-text dl) #f) (make-side new (diff-line-text dl) #f))) (define (meta-row dl) (make-split-row 'meta (make-side #f (diff-line-text dl) #f) #f)) (define (del-row entry old) (make-split-row 'del (entry-side entry old) #f)) (define (add-row entry new) (make-split-row 'add #f (entry-side entry new))) (define (change-row del-entry add-entry old new) (make-split-row 'change (entry-side del-entry old) (entry-side add-entry new))) (define (entry-side entry line) (make-side line (diff-line-text (car entry)) (cdr entry)))