Packages

diff

Compare two texts by line, word or character and show what changed, as a unified diff, as data or as HTML.

import "std:diff"

std:diff finds what changed between two texts, by line, by word or by character. Use it to show a patch, preview an edit before saving it, or highlight changes in a review screen.

osl
import "std:diff"

Compare lines#

diff.text(old, new) returns a Diff. hasChanges says whether the texts differ, and added and removed list the lines that are only in one of them.

main.osl
import "std:diff"

string before = "apple\nbanana\ncherry"
string after = "apple\nblueberry\ncherry\ndate"

auto d = diff.text(before, after)
log d.hasChanges()
log "removed:" d.removed()
log "added:" d.added()
log diff.text(before, before).hasChanges()
Click the code to edit it

Try it: change date back to cherry only and see what added() gives.

Unified format#

unified() writes the familiar patch format: a header giving the line ranges, then every line prefixed with -, + or a space. It is an empty string when nothing changed.

main.osl
import "std:diff"

auto d = diff.text("a\nb\nc\nd", "a\nB\nc\nd\ne")
log d.unified()
log "[" ++ diff.text("same", "same").unified() ++ "]"
Click the code to edit it

How much context#

Only a few unchanged lines are kept around each change, three by default. Change that with context. With 0 you get only the changed lines, and distant changes become separate hunks.

main.osl
import "std:diff"

string[] lines = []
for i in 1 to 12 ( lines.append("line " ++ i) )
string old = lines.join("\n")
lines[2] = "LINE 2"
lines[11] = "LINE 11"
string changed = lines.join("\n")

log diff.text(old, changed, {context: 0}).unified()
log diff.text(old, changed).hunks().len
Click the code to edit it

Hunks as data#

When you want to build your own display, hunks() returns records: where each hunk starts and how long it is in the old and new text, and a list of changes, each with a kind (same, removed or added) and the text.

main.osl
import "std:diff"

for h in diff.text("one\ntwo\nthree", "one\n2\nthree").hunks() (
  log "old from line" h.oldStart "for" h.oldLines "lines"
  for c in h.changes (
    log c.kind ":" c.text
  )
)
Click the code to edit it

Words and characters#

diff.words splits on whitespace, which suits prose. diff.chars compares single characters, which suits short strings such as names or version numbers.

main.osl
import "std:diff"

auto w = diff.words("the quick brown fox", "the slow brown dog")
log w.removed() w.added()

auto c = diff.chars("kitten", "sitting")
log c.removed() c.added()
Click the code to edit it

Try it: compare two sentences of your own with diff.words.

HTML output#

html() writes a div with one child per item, marked diff-same, diff-old or diff-new. Text is escaped. Style those classes to colour a review page.

main.osl
import "std:diff"

log diff.text("a < b\nsame", "a > b\nsame").html()
Click the code to edit it

Only write when changed#

A generator can compare its output with the file on disk and skip the write when nothing differs.

main.osl
import "std:diff"
import "std:fs"

fs.remove("out.txt", {force: true}).unwrap()

def save(string path, string text) (
  if !fs.exists(path) (
    fs.writeFile(path, text).unwrap()
    log path "created"
    return
  )
  string old = fs.readFile(path).unwrap()
  auto d = diff.text(old, text)
  if !d.hasChanges() (
    log path "unchanged"
    return
  )
  fs.writeFile(path, text).unwrap()
  log path "updated, +" ++ d.added().len ++ " -" ++ d.removed().len
)

save("out.txt", "a\nb")
save("out.txt", "a\nb")
save("out.txt", "a\nc\nd")
fs.remove("out.txt").unwrap()
Click the code to edit it
Quick check

How many unchanged lines does diff.text keep around each change by default?

Your turn

What changed on the list?

Print - item for each removed line, then + item for each added one.

main.osl
Ctrl Enter runs
Expected output
- eggs
+ tea

API reference#

15 members, generated from the std:diff source of OSL 9.3.0.

Functions

#diff.chars(string oldText, string newText, object? options) *diff.Diff

chars compares two texts character by character. Options: context (unchanged characters kept around each change, default 0).

#diff.compareLines(string[] oldLines, string[] newLines) *diff.Diff

Deprecated. Deprecated: use diff.text(oldLines.join("\n"), newLines.join("\n")) instead.

compareLines compares two arrays of lines.

#diff.compareWords(string oldText, string newText) *diff.Diff

Deprecated. Deprecated: use diff.words(oldText, newText) instead.

compareWords compares two texts word by word.

#diff.html(*diff.Diff result) string

Deprecated. Deprecated: use result.html() instead.

html formats a diff as HTML in the old style, with a "No differences found" message for equal texts.

#diff.json(*diff.Diff result) string

Deprecated. Deprecated: use json.stringify(result.hunks()) instead.

json encodes a diff's old Hunks, OldText and NewText fields as JSON.

#diff.text(string oldText, string newText, object? options) *diff.Diff

text compares two texts line by line. A newline ends a line, so a text that ends with one has no empty last line. Options: context (unchanged lines kept around each change, default 3).

#diff.tokenize(string text) string[]

Deprecated. Deprecated: split the text yourself, for example with text.split(" ").

tokenize splits text into words on whitespace.

#diff.unified(*diff.Diff result) string

Deprecated. Deprecated: use result.unified() instead.

unified formats a diff in the old style, or "No differences found".

#diff.words(string oldText, string newText, object? options) *diff.Diff

words compares two texts word by word, splitting on whitespace. Options: context (unchanged words kept around each change, default 3).

*diff.Diff

#diff.added() string[]

added returns the lines, words or characters only in the new text, in order.

#diff.hasChanges() boolean

hasChanges reports whether the texts differ.

#diff.html() string

html writes the diff as HTML: a <div class='diff'> holding one escaped <div> per item with class diff-same, diff-old or diff-new. The outer div is empty when the texts are equal.

#diff.hunks() {changes: {kind: string, text: string}[], newLines: int, newStart: int, oldLines: int, oldStart: int}[]

hunks returns the changes grouped into hunks, as records {oldStart, oldLines, newStart, newLines, changes}, where each change is {kind, text} with kind "same", "removed" or "added". Starts are 1-based and the counts are in items (lines, words or characters). [] when the texts are equal.

#diff.removed() string[]

removed returns the lines, words or characters only in the old text, in order.

#diff.unified() string

unified writes the diff in unified format: an "@@ -oldStart,oldLines +newStart,newLines @@" header per hunk, then one item per line prefixed "-" (removed), "+" (added) or " " (unchanged). "" when the texts are equal.