|
121 | 121 | text)) |
122 | 122 |
|
123 | 123 |
|
| 124 | +### Diff functions |
| 125 | + |
| 126 | +(defn- strip-common-prefix |
| 127 | + ``` |
| 128 | + Returns [common-prefix s1-remainder s2-remainder] |
| 129 | + ``` |
| 130 | + [s1 s2] |
| 131 | + (def len (min (length s1) (length s2))) |
| 132 | + (var i 0) |
| 133 | + (while (and (< i len) (= (get s1 i) (get s2 i))) |
| 134 | + (++ i)) |
| 135 | + [(string/slice s1 0 i) (string/slice s1 i) (string/slice s2 i)]) |
| 136 | + |
| 137 | + |
| 138 | +(defn- strip-common-suffix |
| 139 | + ``` |
| 140 | + Returns [s1-remainder s2-remainder common-suffix] |
| 141 | + ``` |
| 142 | + [s1 s2] |
| 143 | + (def len1 (length s1)) |
| 144 | + (def len2 (length s2)) |
| 145 | + (def len (min len1 len2)) |
| 146 | + (var i 0) |
| 147 | + (while (and (< i len) (= (get s1 (- len1 1 i)) (get s2 (- len2 1 i)))) |
| 148 | + (++ i)) |
| 149 | + (if (zero? i) |
| 150 | + [s1 s2 ""] |
| 151 | + [(string/slice s1 0 (- len1 i)) |
| 152 | + (string/slice s2 0 (- len2 i)) |
| 153 | + (string/slice s1 (- len1 i))])) |
| 154 | + |
| 155 | + |
| 156 | +(defn- lcs-table |
| 157 | + ``` |
| 158 | + Builds LCS table for two strings |
| 159 | + ``` |
| 160 | + [s1 s2] |
| 161 | + (def m (length s1)) |
| 162 | + (def n (length s2)) |
| 163 | + (def tbl (array/new-filled (+ m 1) nil)) |
| 164 | + (each i (range 0 (+ m 1)) |
| 165 | + (put tbl i (array/new-filled (+ n 1) 0))) |
| 166 | + (each i (range 1 (+ m 1)) |
| 167 | + (each j (range 1 (+ n 1)) |
| 168 | + (if (= (get s1 (- i 1)) (get s2 (- j 1))) |
| 169 | + (put (tbl i) j (+ (get-in tbl [(- i 1) (- j 1)]) 1)) |
| 170 | + (put (tbl i) j (max (get-in tbl [(- i 1) j]) |
| 171 | + (get-in tbl [i (- j 1)])))))) |
| 172 | + tbl) |
| 173 | + |
| 174 | + |
| 175 | +(defn- traceback-diff |
| 176 | + ``` |
| 177 | + Traces back through LCS table to build diff segments |
| 178 | + ``` |
| 179 | + [tbl s1 s2] |
| 180 | + (def result @[]) |
| 181 | + (var i (length s1)) |
| 182 | + (var j (length s2)) |
| 183 | + (var stype nil) |
| 184 | + (var buf @"") |
| 185 | + (defn flush-segment [] |
| 186 | + (when stype |
| 187 | + (array/push result @{:type stype |
| 188 | + :text (string/reverse buf)}) |
| 189 | + (set buf @""))) |
| 190 | + (while (or (> i 0) (> j 0)) |
| 191 | + (cond |
| 192 | + # characters match: record as equal |
| 193 | + (and (> i 0) (> j 0) (= (get s1 (- i 1)) (get s2 (- j 1)))) |
| 194 | + (do |
| 195 | + (unless (= :equal stype) |
| 196 | + (flush-segment) |
| 197 | + (set stype :equal)) |
| 198 | + (buffer/push-byte buf (get s1 (- i 1))) |
| 199 | + (-- i) |
| 200 | + (-- j)) |
| 201 | + # characters only in s2: record as insertion |
| 202 | + (and (> j 0) (or (zero? i) (>= (get-in tbl [i (- j 1)]) |
| 203 | + (get-in tbl [(- i 1) j])))) |
| 204 | + (do |
| 205 | + (unless (= :insert stype) |
| 206 | + (flush-segment) |
| 207 | + (set stype :insert)) |
| 208 | + (buffer/push-byte buf (get s2 (- j 1))) |
| 209 | + (-- j)) |
| 210 | + # characters only in s1: record as deletion |
| 211 | + (do |
| 212 | + (unless (= stype :delete) |
| 213 | + (flush-segment) |
| 214 | + (set stype :delete)) |
| 215 | + (buffer/push-byte buf (get s1 (- i 1))) |
| 216 | + (-- i)))) |
| 217 | + (flush-segment) |
| 218 | + (reverse result)) |
| 219 | + |
| 220 | + |
| 221 | +(defn- merge-small-changes |
| 222 | + ``` |
| 223 | + Merges small changes for readability |
| 224 | + ``` |
| 225 | + [segments] |
| 226 | + (var i 2) |
| 227 | + (while (def nxt (get segments i)) |
| 228 | + (def prv (get segments (- i 2))) |
| 229 | + (def cur (get segments (- i 1))) |
| 230 | + (cond |
| 231 | + # case 1: skip |
| 232 | + (or (= :equal (prv :type)) |
| 233 | + (= :equal (nxt :type)) |
| 234 | + (not= :equal (cur :type)) |
| 235 | + (string/has-prefix? " " (cur :text)) |
| 236 | + (string/has-suffix? " " (cur :text))) |
| 237 | + (set i (+ i 1)) |
| 238 | + # case 2: prv and nxt different types |
| 239 | + (not= (prv :type) (nxt :type)) |
| 240 | + (do |
| 241 | + (put prv :text (string (prv :text) (cur :text))) |
| 242 | + (put nxt :text (string (cur :text) (nxt :text))) |
| 243 | + (array/remove segments (- i 1)) |
| 244 | + (set i (+ i 1))) |
| 245 | + # case 3: prv and nxt same types |
| 246 | + (def nxt-nxt (get segments (+ i 1))) |
| 247 | + (do |
| 248 | + (put nxt :text (string (cur :text) (nxt :text))) |
| 249 | + (put nxt-nxt :text (string (cur :text) (nxt-nxt :text))) |
| 250 | + (array/remove segments (- i 1)) |
| 251 | + (set i (+ i 2))))) |
| 252 | + (set i 1) |
| 253 | + (while (def nxt (get segments i)) |
| 254 | + (def cur (get segments (- i 1))) |
| 255 | + (if (= (cur :type) (nxt :type)) |
| 256 | + (do |
| 257 | + (put cur :text (string (cur :text) (nxt :text))) |
| 258 | + (array/remove segments i)) |
| 259 | + (++ i))) |
| 260 | + segments) |
| 261 | + |
| 262 | + |
| 263 | +(defn- compute-diff |
| 264 | + ``` |
| 265 | + Computes byte-level diff between two strings with prefix/suffix optimization |
| 266 | + ``` |
| 267 | + [s1 s2] |
| 268 | + (def [prefix s1-mid s2-mid] (strip-common-prefix s1 s2)) |
| 269 | + (def [s1-core s2-core suffix] (strip-common-suffix s1-mid s2-mid)) |
| 270 | + |
| 271 | + (def diff-core |
| 272 | + (if (and (empty? s1-core) (empty? s2-core)) |
| 273 | + @[] |
| 274 | + (let [tbl (lcs-table s1-core s2-core)] |
| 275 | + (traceback-diff tbl s1-core s2-core)))) |
| 276 | + |
| 277 | + (def result @[]) |
| 278 | + (unless (empty? prefix) |
| 279 | + (array/push result {:type :equal :text prefix})) |
| 280 | + (each segment diff-core |
| 281 | + (array/push result segment)) |
| 282 | + (unless (empty? suffix) |
| 283 | + (array/push result {:type :equal :text suffix})) |
| 284 | + |
| 285 | + (merge-small-changes result)) |
| 286 | + |
| 287 | + |
| 288 | +(defn- render-diff-line |
| 289 | + ``` |
| 290 | + Renders a single line from diff data, showing either deletes or inserts |
| 291 | + ``` |
| 292 | + [diff-data show-type] |
| 293 | + (def parts @[]) |
| 294 | + (each segment diff-data |
| 295 | + (case (segment :type) |
| 296 | + :equal |
| 297 | + (array/push parts (segment :text)) |
| 298 | + |
| 299 | + :delete |
| 300 | + (when (= show-type :delete) |
| 301 | + (array/push parts |
| 302 | + (string "\e[48;5;52m" (segment :text) "\e[0m"))) |
| 303 | + |
| 304 | + :insert |
| 305 | + (when (= show-type :insert) |
| 306 | + (array/push parts |
| 307 | + (string "\e[48;5;22m" (segment :text) "\e[0m"))))) |
| 308 | + (string/join parts)) |
| 309 | + |
| 310 | + |
| 311 | +(defn- format-with-diff |
| 312 | + ``` |
| 313 | + Formats expect/actual with diff highlighting |
| 314 | + ``` |
| 315 | + [expect-str actual-str] |
| 316 | + (def diff (compute-diff expect-str actual-str)) |
| 317 | + (def expect-line (render-diff-line diff :delete)) |
| 318 | + (def actual-line (render-diff-line diff :insert)) |
| 319 | + (string "Expect (L): " expect-line "\n" |
| 320 | + "Actual (R): " actual-line)) |
| 321 | + |
| 322 | + |
124 | 323 | (defn- ruler |
125 | 324 | ``` |
126 | 325 | Prints a dashed line as long as the longest line |
|
149 | 348 | [result] |
150 | 349 | (case (result :kind) |
151 | 350 | :equal |
152 | | - (string "Expect (L): " (string/format "%q" (result :expect)) "\n" |
153 | | - "Actual (R): " (string/format "%q" (result :actual))) |
| 351 | + (let [expect-str (string/format "%q" (result :expect)) |
| 352 | + actual-str (string/format "%q" (result :actual))] |
| 353 | + (if (and (dyn :test/color?) |
| 354 | + (< (+ (length expect-str) (length actual-str)) 2000)) |
| 355 | + (format-with-diff expect-str actual-str) |
| 356 | + (string "Expect (L): " expect-str "\n" |
| 357 | + "Actual (R): " actual-str))) |
154 | 358 |
|
155 | 359 | :matches |
156 | 360 | (string "Expect (L): Structure " (string/format "%q" (result :expect)) "\n" |
|
0 commit comments