# diff

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

This is the Markdown version of https://osl-lang.org/packages/diff. Every page of the OSL docs has one: add .md to its path. https://osl-lang.org/llms.txt lists them all.

```osl
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.

```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()
```

Output:

```text
true
removed: ["banana"]
added: ["blueberry","date"]
false
```

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.

```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() ++ "]"
```

Output:

```text
@@ -1,4 +1,5 @@
 a
-b
+B
 c
 d
+e

[]
```

## 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.

```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
```

Output:

```text
@@ -2,1 +2,1 @@
-line 2
+LINE 2
@@ -11,1 +11,1 @@
-line 11
+LINE 11

2
```

## 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`.

```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
  )
)
```

Output:

```text
old from line 1 for 3 lines
same : one
removed : two
added : 2
same : three
```

## 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.

```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()
```

Output:

```text
["quick","fox"] ["slow","dog"]
["k","e"] ["s","i","g"]
```

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.

```osl
import "std:diff"

log diff.text("a < b\nsame", "a > b\nsame").html()
```

Output:

```text
<div class='diff'>
<div class='diff-old'>a &lt; b</div>
<div class='diff-new'>a &gt; b</div>
<div class='diff-same'>same</div>
</div>
```

## Only write when changed

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

```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()
```

Output:

```text
out.txt created
out.txt unchanged
out.txt updated, +2 -1
```

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

- [ ] None.
- [x] Three.
- [ ] All of them.

Answer: Pass `{context: 0}` for only the changes, or a larger number for more around them.

### Exercise: What changed on the list?

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

Starting code:

```osl
import "std:diff"

string before = "milk\neggs\nbread"
string after = "milk\nbread\ntea"
log diff.text(before, after).hasChanges()
```

It should print:

```text
- eggs
+ tea
```

Hint 1: `removed()` and `added()` return arrays of lines.

A solution:

```osl
import "std:diff"

string before = "milk\neggs\nbread"
string after = "milk\nbread\ntea"
auto d = diff.text(before, after)
for item in d.removed() ( log "- " ++ item )
for item in d.added() ( log "+ " ++ item )
```

## API reference

## API reference

Every member of `std:diff`, generated from the source of OSL.

### 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.
