Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 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
9e81930
add MarkdownRenderable with remark-based table alignment
remorses Dec 19, 2025
5380957
use marked
remorses Dec 19, 2025
34ad30c
Create markdown-demo.ts
remorses Dec 19, 2025
06d8d5e
use marked for real
remorses Dec 19, 2025
13079d6
Update markdown-demo.ts
remorses Dec 19, 2025
cfd1fad
render markdown tables using flexbox columns with box borders
remorses Dec 19, 2025
629dfd4
fix conceal toggle by including it in cache key
remorses Dec 19, 2025
6236321
format
remorses Dec 19, 2025
ef97c6f
remove excessive jsdoc comments and fix any cast
remorses Dec 20, 2025
9c5575a
move markdown tests to renderables folder and add more cases, add mar…
remorses Dec 20, 2025
ef8a9a2
simplify code rendering, add custom renderNode tests
remorses Dec 20, 2025
5905d49
remove line numbers from markdown demo
remorses Dec 20, 2025
a9846c0
remove dead code: renderTableChunks, countConcealedChars, getCellDisp…
remorses Dec 20, 2025
886be27
Update markdown-demo.ts
remorses Dec 20, 2025
8046127
fix clearTableCache -> clearCache in demo docs, add incomplete markdo…
remorses Dec 20, 2025
3ef4c61
Merge branch 'main' into markdown-renderable
kommander Dec 22, 2025
7c99258
add examples to index
kommander Dec 22, 2025
7ae9f1e
pin marked version
kommander Dec 22, 2025
0471f1e
reduce recalcs
kommander Dec 22, 2025
4807d6d
add missing registrations
remorses Dec 22, 2025
9c79532
fix test
remorses Dec 22, 2025
c89a3da
streaming markdown support
remorses Dec 22, 2025
bc06339
optimize table rendering during streaming
remorses Dec 22, 2025
71b12c1
clean up comments
remorses Dec 22, 2025
d558a40
fix formatting
remorses Dec 22, 2025
6cfa2f4
raw fallback for incomplete rows
kommander Dec 22, 2025
89c548d
streaming in demo
kommander Dec 22, 2025
b1d4bfd
demo stream speed control
kommander Dec 22, 2025
0d800bf
demo sticky scroll for stream
kommander Dec 22, 2025
7b84b78
fix conceal and syntaxStyle changes not updating rendered content
remorses Dec 22, 2025
6be878f
improve table border joins at top and bottom edges
remorses Dec 22, 2025
756b85f
perf: defer style/conceal re-rendering to renderSelf
remorses Dec 23, 2025
816b7d6
focus scrollbox in demo
remorses Dec 23, 2025
b06925e
fix: format Markdown.ts with prettier
remorses Dec 23, 2025
602c91a
update tables in-place
kommander Dec 27, 2025
99daa83
demo endless stream
kommander Dec 27, 2025
f92de6d
no need to reset
kommander Dec 27, 2025
1ef2376
Merge branch 'main' into markdown-renderable
remorses Jan 2, 2026
c966950
Merge branch 'main' into markdown-renderable
kommander Jan 5, 2026
5c6ae8b
Merge branch 'main' into markdown-renderable
remorses Jan 7, 2026
35e6ad4
Merge anomalyco/main into word-highlights
remorses Jan 7, 2026
5265e66
Merge branch 'main' into word-highlights
kommander Jan 8, 2026
513921d
Merge upstream/main
remorses Jan 11, 2026
cf245be
Merge branch 'main' into markdown-renderable
remorses Jan 14, 2026
51f13a5
fix(ci): fix pkg-pr-new workflow for native binary packages
remorses Jan 14, 2026
add884e
Merge fix-pkg-pr-new
remorses Jan 15, 2026
203f75b
Merge branch 'main' into markdown-renderable
remorses Jan 15, 2026
77f314b
Merge word-highlights into critique
remorses Jan 16, 2026
e268e3d
Merge remote-tracking branch 'upstream/main' into word-highlights
remorses Jan 16, 2026
2d2ed77
refactor: use GitHub Desktop-style word highlights algorithm
remorses Jan 16, 2026
070b0cd
Merge word-highlights into critique
remorses Jan 16, 2026
16a4b54
use conceal for tables border
remorses Jan 16, 2026
a79c0c5
Merge branch 'markdown-renderable' of https://github.com/remorses/ope…
remorses Jan 16, 2026
7a09804
tweak word highlights
remorses Jan 16, 2026
302acd5
Merge markdown-renderable into critique
remorses Jan 16, 2026
c44a164
reduce word highlight brightness, use multiplicative alpha
remorses Jan 21, 2026
a52878a
Merge remote-tracking branch 'upstream/main' into word-highlights
remorses Jan 21, 2026
367a940
Merge branch 'word-highlights' into critique
remorses Jan 22, 2026
a43bdee
fix(core): skip stdout writes in testing mode for writeOut functions
remorses Jan 23, 2026
950100c
Merge branch 'fix-test-renderer-stdout' into critique
remorses Jan 23, 2026
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
211 changes: 209 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, computeInlineHighlights, relativeChanges, MaxIntraLineDiffStringLength } from "./Diff"
import { SyntaxStyle } from "../syntax-style"
import { RGBA } from "../lib/RGBA"
import { createTestRenderer, type TestRenderer } from "../testing"
Expand Down Expand Up @@ -2668,6 +2668,213 @@ test("DiffRenderable - fg prop accepts RGBA directly", async () => {
expect(leftCodeRenderable.fg).toEqual(customFg)
})

describe("relativeChanges", () => {
test("returns empty ranges for identical strings", () => {
const result = relativeChanges("hello world", "hello world")
expect(result.stringARange.length).toBe(0)
expect(result.stringBRange.length).toBe(0)
})

test("returns full ranges for completely different strings", () => {
const result = relativeChanges("abc", "xyz")
expect(result.stringARange).toEqual({ location: 0, length: 3 })
expect(result.stringBRange).toEqual({ location: 0, length: 3 })
})

test("finds changed region with common prefix", () => {
const result = relativeChanges("hello world", "hello there")
// Common prefix: "hello " (6 chars)
expect(result.stringARange.location).toBe(6)
expect(result.stringARange.length).toBe(5) // "world"
expect(result.stringBRange.location).toBe(6)
expect(result.stringBRange.length).toBe(5) // "there"
})

test("finds changed region with common suffix", () => {
const result = relativeChanges("const x = 1", "const x = 2")
// Common prefix: "const x = " (10 chars), Common suffix: "" (0 chars)
expect(result.stringARange.location).toBe(10)
expect(result.stringARange.length).toBe(1) // "1"
expect(result.stringBRange.location).toBe(10)
expect(result.stringBRange.length).toBe(1) // "2"
})

test("handles empty strings", () => {
const result1 = relativeChanges("hello", "")
expect(result1.stringARange).toEqual({ location: 0, length: 5 })
expect(result1.stringBRange).toEqual({ location: 0, length: 0 })

const result2 = relativeChanges("", "hello")
expect(result2.stringARange).toEqual({ location: 0, length: 0 })
expect(result2.stringBRange).toEqual({ location: 0, length: 5 })
})

test("finds single contiguous changed region (not multiple)", () => {
// "a b c" -> "x b z" has changes at start and end
// But prefix/suffix algorithm returns single region from first change to last
const result = relativeChanges("a b c", "x b z")
// Common prefix: "" (0 chars), Common suffix: "" (0 chars)
// So everything is different
expect(result.stringARange.location).toBe(0)
expect(result.stringARange.length).toBe(5)
expect(result.stringBRange.location).toBe(0)
expect(result.stringBRange.length).toBe(5)
})
})

describe("computeInlineHighlights", () => {
test("returns null highlights for identical strings", () => {
const result = computeInlineHighlights("hello world", "hello world")
expect(result.oldHighlight).toBeNull()
expect(result.newHighlight).toBeNull()
})

test("highlights changed region", () => {
const result = computeInlineHighlights("hello world", "hello there")
expect(result.oldHighlight).not.toBeNull()
expect(result.oldHighlight!.type).toBe("removed-word")
expect(result.newHighlight).not.toBeNull()
expect(result.newHighlight!.type).toBe("added-word")
})

test("computes correct column positions", () => {
const result = computeInlineHighlights("const x = 1", "const x = 2")
expect(result.oldHighlight!.startCol).toBe(10)
expect(result.oldHighlight!.endCol).toBe(11)
expect(result.newHighlight!.startCol).toBe(10)
expect(result.newHighlight!.endCol).toBe(11)
})

test("returns single contiguous region for multiple changes (GitHub Desktop behavior)", () => {
// Unlike word-level diffing, prefix/suffix algorithm returns single region
const result = computeInlineHighlights("a b c", "x b z")
// Single highlight covering the entire changed region
expect(result.oldHighlight).not.toBeNull()
expect(result.newHighlight).not.toBeNull()
expect(result.oldHighlight!.startCol).toBe(0)
expect(result.oldHighlight!.endCol).toBe(5) // "a b c"
expect(result.newHighlight!.startCol).toBe(0)
expect(result.newHighlight!.endCol).toBe(5) // "x b z"
})

test("handles multi-width characters (CJK)", () => {
const result = computeInlineHighlights("hello 世界", "hello 你好")
expect(result.oldHighlight).not.toBeNull()
expect(result.newHighlight).not.toBeNull()
expect(result.oldHighlight!.startCol).toBe(6)
expect(result.oldHighlight!.endCol).toBe(10) // 2 CJK chars = 4 display width
expect(result.newHighlight!.startCol).toBe(6)
expect(result.newHighlight!.endCol).toBe(10)
})

test("handles emoji characters", () => {
const result = computeInlineHighlights("test 👍", "test 👎")
expect(result.oldHighlight).not.toBeNull()
expect(result.newHighlight).not.toBeNull()
expect(result.oldHighlight!.startCol).toBe(5)
expect(result.newHighlight!.startCol).toBe(5)
})

test("handles insertion (no removal)", () => {
const result = computeInlineHighlights("hello", "hello world")
expect(result.oldHighlight).toBeNull() // nothing removed
expect(result.newHighlight).not.toBeNull()
expect(result.newHighlight!.startCol).toBe(5)
expect(result.newHighlight!.endCol).toBe(11) // " world"
})

test("handles deletion (no addition)", () => {
const result = computeInlineHighlights("hello world", "hello")
expect(result.oldHighlight).not.toBeNull()
expect(result.oldHighlight!.startCol).toBe(5)
expect(result.oldHighlight!.endCol).toBe(11) // " world"
expect(result.newHighlight).toBeNull() // nothing added
})
})

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.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("only highlights when equal number of adds and removes (GitHub Desktop behavior)", async () => {
const syntaxStyle = SyntaxStyle.fromStyles({
default: { fg: RGBA.fromValues(1, 1, 1, 1) },
})

// This diff has 1 remove and 1 add - should highlight
const equalDiff = `--- a/test.js
+++ b/test.js
@@ -1 +1 @@
-const x = 1
+const x = 2
`

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

// The diff should be rendered (we can't easily check highlights without more infrastructure)
expect(diffRenderable.disableWordHighlights).toBe(false)
})

test("MaxIntraLineDiffStringLength is exported and has correct value", () => {
expect(MaxIntraLineDiffStringLength).toBe(1024)
})
})

test("DiffRenderable - split view with word wrapping: changing diff content should not misalign sides", async () => {
const { BoxRenderable } = await import("./Box")
const { parseColor } = await import("../lib/RGBA")
Expand Down
Loading
Loading