#+private package diff /* The `.Compact` post-pass. It slides each group of changes along equal tokens to where git puts it: beside a change on the other side if it can, otherwise as far down as it goes. With `.Indent_Heuristic` it picks a position by the indentation and blank lines around it instead. With `.Histogram` each group that moved or grew is diffed again against its partner. A group is a maximal run of changed tokens on one side, and may be empty. A side with `u` unchanged tokens has `u + 1` groups: one before each unchanged token, and one after the last. The `k`-th group of one side is the partner of the `k`-th group of the other. Both sit between the same pair of unchanged tokens. */ // The indent of a line of only whitespace. LINE_INDENT_BLANK :: 255 // An indent is never measured past this. LINE_INDENT_MAX :: 200 // A tab moves to the next multiple of this. LINE_INDENT_TAB :: 8 // The indent heuristic tries a group at most this many positions above the bottom. COMPACT_MAX_SLIDING :: 100 // The indent heuristic looks past at most this many blank lines. COMPACT_MAX_BLANKS :: 20 // The weights of the indent heuristic. A lower score is a better place to split. INDENT_START_OF_FILE :: 1 INDENT_END_OF_FILE :: 21 INDENT_BLANKS :: -30 // for each blank line around the split INDENT_BLANKS_AFTER :: 6 // for each blank line after the split INDENT_DEEPER :: -4 // indented more than above INDENT_DEEPER_BLANKS :: 10 INDENT_BLOCK_START :: 24 // indented less than above, but more below INDENT_BLOCK_START_BLANKS :: 17 INDENT_BLOCK_END :: 23 // indented less than above INDENT_BLOCK_END_BLANKS :: 17 INDENT_EFFECTIVE_WEIGHT :: 60 // for the sign of a difference in effective indent // The options `rediff` diffs with, apart from `max_cost`. REDIFF_OPTIONS :: Options{ algorithm = .Myers, flags = {.Prune}, } // One side of a diff, for `.Compact`. Compact_Side :: struct { ids: []i32, offset: int, // where this side's bits start in `Changes.bits` indents: []u8, // one per token, for `.Indent_Heuristic`; nil otherwise } // The changed tokens `[start, end)` of one side. Group :: struct { start, end: int, } // A group of one side and its partner, walked through both sides in step. Compact_Walk :: struct { c: Changes, this, other: Compact_Side, g, go: Group, } // How good a place to split the indent heuristic finds. Lower is better. Indent_Score :: struct { effective_indent: int, penalty: int, } /* Measures the indent of `line`, its final "\n" included. A space adds 1, a tab moves to the next multiple of `LINE_INDENT_TAB`, and the other whitespace adds nothing. Returns at most `LINE_INDENT_MAX`, or `LINE_INDENT_BLANK` for a line of only whitespace. */ @(require_results) line_indent :: proc "contextless" (line: string) -> u8 { indent := 0 for i in 0..= LINE_INDENT_MAX { return LINE_INDENT_MAX } } return LINE_INDENT_BLANK } // Compacts `a`'s side against `b`, then `b`'s side against `a`. compact :: proc(a, b: []i32, indents: []u8, id_count: int, changes: Changes, s: ^Scratch, opts: Options, loc := #caller_location) { a_side := Compact_Side{a, 0, nil} b_side := Compact_Side{b, _inserted_offset(changes), nil} if indents != nil { assert(len(indents) == len(a) + len(b), "an indent is needed for every line", loc) a_side.indents = indents[:len(a)] b_side.indents = indents[len(a):] } // Diffing merged groups again is git's for histogram only rediffing := opts.algorithm == .Histogram compact_side(changes, a_side, b_side, rediffing, id_count, opts.max_cost, s, loc) compact_side(changes, b_side, a_side, rediffing, id_count, opts.max_cost, s, loc) } // The scratch `rediff` takes for any groups within `n` by `m` tokens. @(require_results) rediff_scratch_size :: proc "contextless" (n, m: int, id_count: int) -> int { return scratch_bytes(u64, changes_size(n, m)) + prune_scratch_size(n, m, id_count, REDIFF_OPTIONS) } /* Diffs `this.ids[g.start:g.end]` against `other.ids[go.start:go.end]` again, with Myers after `.Prune`'s preparation. Every bit of both groups is then set or cleared: `this` takes the deletions, and `other` the insertions. Gives back its scratch. */ rediff :: proc(c: Changes, this, other: Compact_Side, g, go: Group, id_count: int, max_cost: int, s: ^Scratch, loc := #caller_location) { // Copies `count` bits from `src[src_lo:]` to `dst[dst_lo:]` copy_bits :: proc "contextless" (dst: []u64, dst_lo: int, src: []u64, src_lo: int, count: int) { for t in 0..= 0: // Keep a replacement together, rather than a deletion apart from an insertion for w.go.start == w.go.end { compact_slide_up(&w) } case this.indents != nil: target := indent_best_end(this.indents, w.g, earliest_end) for w.g.end > target { compact_slide_up(&w) } } if rediffing && w.go.start < w.go.end && w.g != was { rediff(c, this, other, w.g, w.go, id_count, max_cost, s, loc) } } compact_next_group(c, this, &w.g) or_break partner_moved := compact_next_group(c, other, &w.go) assert(partner_moved, "changes have more unchanged tokens on one side", loc) } assert(w.go.end == len(other.ids), "changes have more unchanged tokens on one side", loc) } compact_put_bit :: #force_inline proc "contextless" (bits: []u64, i: int, set: bool) { mask := u64(1) << uint(i & 63) if set { bits[i >> 6] |= mask } else { bits[i >> 6] &~= mask } } // The group before the first unchanged token of `side`. @(require_results) compact_first_group :: proc "contextless" (c: Changes, side: Compact_Side) -> Group { return {0, _bit_run(c.bits, side.offset, side.offset + len(side.ids), true)} } // Moves `g` to the next group of `side`, just past the unchanged token that ends it. False at the last group. @(require_results) compact_next_group :: proc "contextless" (c: Changes, side: Compact_Side, g: ^Group) -> bool { n := len(side.ids) if g.end >= n { return false } g.start = g.end + 1 g.end = g.start + _bit_run(c.bits, side.offset + g.start, side.offset + n, true) return true } // Moves `g` to the previous group of `side`, just before the unchanged token that starts it. False at the first. @(require_results) compact_prev_group :: proc "contextless" (c: Changes, side: Compact_Side, g: ^Group) -> bool { if g.start <= 0 { return false } g.end = g.start - 1 g.start = compact_run_start(c, side, g.end) return true } // Where the run of changed tokens ending just before `end` starts. @(require_results) compact_run_start :: proc "contextless" (c: Changes, side: Compact_Side, end: int) -> (start: int) { start = end for start > 0 && bit_get(c.bits, side.offset + start - 1) { start -= 1 } return } @(require_results) compact_can_slide_down :: proc "contextless" (w: ^Compact_Walk) -> bool { ids := w.this.ids return w.g.start < w.g.end && w.g.end < len(ids) && ids[w.g.end] == ids[w.g.start] } @(require_results) compact_can_slide_up :: proc "contextless" (w: ^Compact_Walk) -> bool { ids := w.this.ids return w.g.start < w.g.end && w.g.start > 0 && ids[w.g.start-1] == ids[w.g.end-1] } // Slides the group down one token, taking in any group it then touches. The partner moves to the next group. compact_slide_down :: proc "contextless" (w: ^Compact_Walk) { offset := w.this.offset n := len(w.this.ids) compact_put_bit(w.c.bits, offset + w.g.start, false) compact_put_bit(w.c.bits, offset + w.g.end, true) w.g.start += 1 w.g.end += 1 w.g.end += _bit_run(w.c.bits, offset + w.g.end, offset + n, true) moved := compact_next_group(w.c, w.other, &w.go) assert_contextless(moved, "changes have more unchanged tokens on one side") } // Slides the group up one token, taking in any group it then touches. The partner moves to the previous group. compact_slide_up :: proc "contextless" (w: ^Compact_Walk) { offset := w.this.offset compact_put_bit(w.c.bits, offset + w.g.start - 1, true) compact_put_bit(w.c.bits, offset + w.g.end - 1, false) w.g.end -= 1 w.g.start = compact_run_start(w.c, w.this, w.g.start) moved := compact_prev_group(w.c, w.other, &w.go) assert_contextless(moved, "changes have more unchanged tokens on one side") } /* The indent heuristic. Tries the group `g`, at the bottom, at each end from up to `COMPACT_MAX_SLIDING` above down to where it is, and returns the end that scores best. Ties go to the later, lower end. */ @(require_results) indent_best_end :: proc "contextless" (indents: []u8, g: Group, earliest_end: int) -> (best_end: int) { size := g.end - g.start best := Indent_Score{} best_end = -1 for end := max(earliest_end, g.end - size - 1, g.end - COMPACT_MAX_SLIDING); end <= g.end; end += 1 { score := Indent_Score{} indent_score_split(indents, end, &score) indent_score_split(indents, end - size, &score) if best_end < 0 || indent_compare(score, best) <= 0 { best = score best_end = end } } return } // Negative when `x` is better than `y`, and zero when they are as good. @(require_results) indent_compare :: proc "contextless" (x, y: Indent_Score) -> int { sign := 0 switch { case x.effective_indent > y.effective_indent: sign = 1 case x.effective_indent < y.effective_indent: sign = -1 } return INDENT_EFFECTIVE_WEIGHT*sign + (x.penalty - y.penalty) } /* Adds to `score` for a split just above line `s`, from the indents of the lines around it. A blank line has indent -1, and so does the line past the end. */ indent_score_split :: proc "contextless" (indents: []u8, s: int, score: ^Indent_Score) { @(require_results) indent_of :: #force_inline proc "contextless" (x: u8) -> int { if x == LINE_INDENT_BLANK { return -1 } return int(x) } n := len(indents) // The line itself end_of_file := s >= n indent := -1 if !end_of_file { indent = indent_of(indents[s]) } // The first line above that is not blank, looking past `COMPACT_MAX_BLANKS` blank lines at most pre_blank := 0 pre_indent := -1 for i := s - 1; i >= 0; i -= 1 { if indents[i] != LINE_INDENT_BLANK { pre_indent = int(indents[i]) break } pre_blank += 1 if pre_blank == COMPACT_MAX_BLANKS { pre_indent = 0 break } } // The same below, from the line after it post_blank := 0 post_indent := -1 for i := s + 1; i < n; i += 1 { if indents[i] != LINE_INDENT_BLANK { post_indent = int(indents[i]) break } post_blank += 1 if post_blank == COMPACT_MAX_BLANKS { post_indent = 0 break } } if pre_indent == -1 && pre_blank == 0 { score.penalty += INDENT_START_OF_FILE } if end_of_file { score.penalty += INDENT_END_OF_FILE } // The blank lines after the split, counting the line itself post := 0 if indent == -1 { post = 1 + post_blank } total := pre_blank + post score.penalty += INDENT_BLANKS*total + INDENT_BLANKS_AFTER*post ind := indent if indent == -1 { ind = post_indent } score.effective_indent += ind blanks := total != 0 switch { case ind == -1 || pre_indent == -1 || ind == pre_indent: // Nothing to say case ind > pre_indent: score.penalty += blanks ? INDENT_DEEPER_BLANKS : INDENT_DEEPER case post_indent != -1 && post_indent > ind: score.penalty += blanks ? INDENT_BLOCK_START_BLANKS : INDENT_BLOCK_START case: score.penalty += blanks ? INDENT_BLOCK_END_BLANKS : INDENT_BLOCK_END } }