Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ec432ca
add word highlights
remorses Nov 30, 2025
c0ceee1
test: add word highlight tests
remorses Nov 30, 2025
85e2afd
format
remorses Nov 30, 2025
45884fe
fix: use Bun.stringWidth for multi-width character support in word hi…
remorses Nov 30, 2025
ebfc48c
fix: correct line counts in example diff hunk header
remorses Nov 30, 2025
a5a9136
update react example
remorses Nov 30, 2025
d663823
rename showWordHighlights to disableWordHighlights
remorses Nov 30, 2025
117e470
chore: remove verbose comments
remorses Dec 5, 2025
1c2e986
refactor: simplify word highlights code
remorses Dec 5, 2025
6cc3958
fix: revert unrelated simplifications to split view forEach loops
remorses Dec 5, 2025
4a96d11
Merge upstream/main into word-highlights
remorses Dec 14, 2025
88080c0
chore: format Diff.ts
remorses Dec 14, 2025
912849e
fix: word highlights scroll with content when scrollX is non-zero
remorses Dec 14, 2025
f437756
fix: word highlights respect line wrapping
remorses Dec 14, 2025
8989ff0
fix: correctly compute column offset for wrapped line highlights
remorses Dec 14, 2025
8e63649
cleanup: remove excessive comments
remorses Dec 14, 2025
cbc18de
test: use inline snapshots with trimmed lines for scrollX tests
remorses Dec 14, 2025
212bec3
test: remove obsolete external snapshots
remorses Dec 14, 2025
d3175de
test: remove useless word highlight tests that don't test anything me…
remorses Dec 14, 2025
9a100ea
test: remove scrollX tests that can't verify highlight rendering
remorses Dec 14, 2025
570e067
chore: format
remorses Dec 14, 2025
4da4af0
Merge branch 'main' into word-highlights
remorses Dec 16, 2025
0b2ef8b
use diff with words for similarity
remorses Dec 16, 2025
b00521e
Merge branch 'word-highlights' of https://github.com/remorses/opentui…
remorses Dec 16, 2025
802361d
increase default lineSimilarityThreshold to 0.5
remorses Dec 16, 2025
e74a3fa
fix test: update expected default lineSimilarityThreshold to 0.5
remorses Dec 16, 2025
c650652
Merge branch 'main' into word-highlights
remorses Dec 18, 2025
a8ceb5e
derive word highlight colors from hunk colors with brighten + opacity…
remorses Dec 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 160 additions & 2 deletions packages/core/src/renderables/Diff.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { test, expect, beforeEach, afterEach } from "bun:test"
import { DiffRenderable } from "./Diff"
import { test, expect, beforeEach, afterEach, describe } from "bun:test"
import { DiffRenderable, computeLineSimilarity, computeInlineHighlights } from "./Diff"
import { SyntaxStyle } from "../syntax-style"
import { RGBA } from "../lib/RGBA"
import { createTestRenderer, type TestRenderer } from "../testing"
Expand Down Expand Up @@ -2667,3 +2667,161 @@ test("DiffRenderable - fg prop accepts RGBA directly", async () => {
const leftCodeRenderable = (diffRenderable as any).leftCodeRenderable
expect(leftCodeRenderable.fg).toEqual(customFg)
})

describe("computeLineSimilarity", () => {
test("returns 1.0 for identical strings", () => {
expect(computeLineSimilarity("hello world", "hello world")).toBe(1.0)
})

test("returns 1.0 for both empty strings", () => {
expect(computeLineSimilarity("", "")).toBe(1.0)
})

test("returns 0.0 when one string is empty", () => {
expect(computeLineSimilarity("hello", "")).toBe(0.0)
expect(computeLineSimilarity("", "hello")).toBe(0.0)
})

test("returns high similarity for small changes", () => {
const similarity = computeLineSimilarity("const x = 1", "const x = 2")
expect(similarity).toBeGreaterThan(0.8)
})

test("returns low similarity for completely different strings", () => {
const similarity = computeLineSimilarity("abc", "xyz")
expect(similarity).toBe(0.0)
})

test("returns partial similarity for partially matching strings", () => {
const similarity = computeLineSimilarity("hello world", "hello there")
expect(similarity).toBeGreaterThan(0.4)
expect(similarity).toBeLessThan(0.7)
})
})

describe("computeInlineHighlights", () => {
test("returns empty highlights for identical strings", () => {
const result = computeInlineHighlights("hello world", "hello world")
expect(result.oldHighlights).toHaveLength(0)
expect(result.newHighlights).toHaveLength(0)
})

test("highlights changed words", () => {
const result = computeInlineHighlights("hello world", "hello there")
expect(result.oldHighlights.length).toBeGreaterThan(0)
expect(result.oldHighlights[0].type).toBe("removed-word")
expect(result.newHighlights.length).toBeGreaterThan(0)
expect(result.newHighlights[0].type).toBe("added-word")
})

test("computes correct column positions", () => {
const result = computeInlineHighlights("const x = 1", "const x = 2")
expect(result.oldHighlights[0].startCol).toBe(10)
expect(result.oldHighlights[0].endCol).toBe(11)
expect(result.newHighlights[0].startCol).toBe(10)
expect(result.newHighlights[0].endCol).toBe(11)
})

test("handles multiple changes", () => {
const result = computeInlineHighlights("a b c", "x b z")
expect(result.oldHighlights.length).toBe(2)
expect(result.newHighlights.length).toBe(2)
})

test("handles multi-width characters (CJK)", () => {
const result = computeInlineHighlights("hello 世界", "hello 你好")
expect(result.oldHighlights.length).toBe(1)
expect(result.newHighlights.length).toBe(1)
expect(result.oldHighlights[0].startCol).toBe(6)
expect(result.oldHighlights[0].endCol).toBe(10)
expect(result.newHighlights[0].startCol).toBe(6)
expect(result.newHighlights[0].endCol).toBe(10)
})

test("handles emoji characters", () => {
const result = computeInlineHighlights("test 👍", "test 👎")
expect(result.oldHighlights.length).toBe(1)
expect(result.newHighlights.length).toBe(1)
expect(result.oldHighlights[0].startCol).toBe(5)
expect(result.newHighlights[0].startCol).toBe(5)
})
})

describe("DiffRenderable word highlights", () => {
test("word highlight options have correct defaults", async () => {
const syntaxStyle = SyntaxStyle.fromStyles({
default: { fg: RGBA.fromValues(1, 1, 1, 1) },
})

const diffRenderable = new DiffRenderable(currentRenderer, {
id: "test-diff",
diff: simpleDiff,
view: "split",
syntaxStyle,
})

expect(diffRenderable.disableWordHighlights).toBe(false)
expect(diffRenderable.lineSimilarityThreshold).toBe(0.5)
expect(diffRenderable.addedWordBg).toBeDefined()
expect(diffRenderable.removedWordBg).toBeDefined()
})

test("can disable word highlights", async () => {
const syntaxStyle = SyntaxStyle.fromStyles({
default: { fg: RGBA.fromValues(1, 1, 1, 1) },
})

const diffRenderable = new DiffRenderable(currentRenderer, {
id: "test-diff",
diff: simpleDiff,
view: "split",
syntaxStyle,
disableWordHighlights: true,
})

expect(diffRenderable.disableWordHighlights).toBe(true)
diffRenderable.disableWordHighlights = false
expect(diffRenderable.disableWordHighlights).toBe(false)
})

test("can customize word highlight colors", async () => {
const syntaxStyle = SyntaxStyle.fromStyles({
default: { fg: RGBA.fromValues(1, 1, 1, 1) },
})

const diffRenderable = new DiffRenderable(currentRenderer, {
id: "test-diff",
diff: simpleDiff,
view: "split",
syntaxStyle,
addedWordBg: "#00ff00",
removedWordBg: "#ff0000",
})

expect(diffRenderable.addedWordBg).toEqual(RGBA.fromHex("#00ff00"))
expect(diffRenderable.removedWordBg).toEqual(RGBA.fromHex("#ff0000"))
})

test("can adjust similarity threshold", async () => {
const syntaxStyle = SyntaxStyle.fromStyles({
default: { fg: RGBA.fromValues(1, 1, 1, 1) },
})

const diffRenderable = new DiffRenderable(currentRenderer, {
id: "test-diff",
diff: simpleDiff,
view: "split",
syntaxStyle,
lineSimilarityThreshold: 0.8,
})

expect(diffRenderable.lineSimilarityThreshold).toBe(0.8)
diffRenderable.lineSimilarityThreshold = 0.5
expect(diffRenderable.lineSimilarityThreshold).toBe(0.5)
diffRenderable.lineSimilarityThreshold = 1.5
expect(diffRenderable.lineSimilarityThreshold).toBe(1.0)

diffRenderable.lineSimilarityThreshold = -0.5
expect(diffRenderable.lineSimilarityThreshold).toBe(0.0)
})
})
Loading
Loading