|
65 | 65 | #' Returns NULL when the two inputs are byte-identical (signal to the |
66 | 66 | #' caller that no diff display is warranted). When `diff` isn't on PATH, |
67 | 67 | #' returns a fallback payload describing the size of the change without |
68 | | -#' the per-line content. |
| 68 | +#' the per-line content. Large diffs are truncated to keep the payload |
| 69 | +#' bounded — both for serialization across the callr worker boundary |
| 70 | +#' and for chat scrollback hygiene — but the `summary` counts always |
| 71 | +#' reflect the full diff. |
69 | 72 | #' |
70 | 73 | #' @param old_text Character scalar, prior file contents. Empty string |
71 | 74 | #' means "new file". |
72 | 75 | #' @param new_text Character scalar, new file contents. |
73 | 76 | #' @param path Character scalar, the file path the diff describes; used |
74 | 77 | #' for the `+++` / `---` header labels. |
| 78 | +#' @param max_lines Cap on the number of diff lines retained for |
| 79 | +#' display. Beyond this, a `[diff truncated: N more lines]` marker is |
| 80 | +#' appended in place of the rest. Set to `Inf` to disable. |
| 81 | +#' @param max_chars Cap on total characters across retained lines. |
| 82 | +#' Tripped if a small number of long lines blow past the budget even |
| 83 | +#' though `max_lines` hasn't. |
75 | 84 | #' @return NULL if identical, else a list with: |
76 | 85 | #' \itemize{ |
77 | 86 | #' \item \code{path}: input path |
78 | | -#' \item \code{summary}: one-line summary string |
| 87 | +#' \item \code{summary}: one-line summary string (always reflects |
| 88 | +#' the full diff, not the truncated lines) |
79 | 89 | #' \item \code{lines}: character vector of uncolored diff lines |
80 | 90 | #' (header + hunks). May be empty when only the fallback |
81 | | -#' summary is available. |
| 91 | +#' summary is available, or truncated for large diffs. |
82 | 92 | #' \item \code{fallback}: logical TRUE when `diff` was unavailable |
83 | 93 | #' and the payload is summary-only. |
| 94 | +#' \item \code{truncated}: logical TRUE when `lines` was clipped. |
84 | 95 | #' } |
85 | 96 | #' @noRd |
86 | | -compute_unified_diff <- function(old_text, new_text, path) { |
| 97 | +compute_unified_diff <- function(old_text, new_text, path, |
| 98 | + max_lines = 200L, |
| 99 | + max_chars = 20000L) { |
87 | 100 | old_text <- old_text %||% "" |
88 | 101 | new_text <- new_text %||% "" |
89 | 102 | path <- path %||% "(unnamed)" |
@@ -148,10 +161,52 @@ compute_unified_diff <- function(old_text, new_text, path) { |
148 | 161 | } |
149 | 162 |
|
150 | 163 | counts <- .diff_summary_counts(res) |
| 164 | + full_lines <- as.character(res) |
| 165 | + clipped <- .clip_diff_lines(full_lines, max_lines, max_chars) |
151 | 166 | list(path = path, |
152 | 167 | summary = .diff_summary_line(counts$added, counts$removed), |
153 | | - lines = as.character(res), |
154 | | - fallback = FALSE) |
| 168 | + lines = clipped$lines, |
| 169 | + fallback = FALSE, |
| 170 | + truncated = clipped$truncated) |
| 171 | +} |
| 172 | + |
| 173 | +#' Clip a diff-line vector to the configured budgets. |
| 174 | +#' |
| 175 | +#' Returns a list with the (possibly clipped) `lines` and a `truncated` |
| 176 | +#' flag. When the budget is busted we keep the first N lines and append |
| 177 | +#' a `[diff truncated: N more lines]` marker so the reader knows there |
| 178 | +#' was more. |
| 179 | +#' @noRd |
| 180 | +.clip_diff_lines <- function(lines, max_lines, max_chars) { |
| 181 | + total <- length(lines) |
| 182 | + if (total == 0L) { |
| 183 | + return(list(lines = lines, truncated = FALSE)) |
| 184 | + } |
| 185 | + |
| 186 | + keep <- min(total, as.integer(max_lines)) |
| 187 | + head <- lines[seq_len(keep)] |
| 188 | + |
| 189 | + # Character budget: walk the kept lines until we'd exceed max_chars, |
| 190 | + # then drop the rest. Counts newlines so the budget matches what |
| 191 | + # the user actually sees. |
| 192 | + if (is.finite(max_chars)) { |
| 193 | + widths <- nchar(head, type = "bytes") + 1L |
| 194 | + running <- cumsum(widths) |
| 195 | + within <- which(running <= as.integer(max_chars)) |
| 196 | + keep_chars <- if (length(within) == 0L) 0L else max(within) |
| 197 | + if (keep_chars < length(head)) { |
| 198 | + head <- head[seq_len(keep_chars)] |
| 199 | + } |
| 200 | + } |
| 201 | + |
| 202 | + truncated <- length(head) < total |
| 203 | + if (truncated) { |
| 204 | + dropped <- total - length(head) |
| 205 | + head <- c(head, |
| 206 | + sprintf("[diff truncated: %d more line%s]", |
| 207 | + dropped, if (dropped == 1L) "" else "s")) |
| 208 | + } |
| 209 | + list(lines = head, truncated = truncated) |
155 | 210 | } |
156 | 211 |
|
157 | 212 | #' Render a diff payload to the terminal. |
|
0 commit comments