summaryrefslogtreecommitdiffhomepage
path: root/packages/lsp/src/diff.test.ts
blob: b5b6a7b551437c1994ec99bbe0994b0aead15d62 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import { describe, expect, it } from "vitest";
import { computeChangeRange, offsetToPosition } from "./diff.js";

describe("offsetToPosition", () => {
	it("returns 0:0 for offset 0", () => {
		expect(offsetToPosition("hello", 0)).toEqual({ line: 0, character: 0 });
	});

	it("counts characters on the first line", () => {
		expect(offsetToPosition("hello", 3)).toEqual({ line: 0, character: 3 });
	});

	it("resets character count after newline", () => {
		expect(offsetToPosition("ab\ncd", 4)).toEqual({ line: 1, character: 1 });
	});

	it("handles multiple lines", () => {
		expect(offsetToPosition("a\nb\nc", 4)).toEqual({ line: 2, character: 0 });
	});

	it("clamps offset beyond text length", () => {
		expect(offsetToPosition("ab", 100)).toEqual({ line: 0, character: 2 });
	});

	it("handles empty string", () => {
		expect(offsetToPosition("", 0)).toEqual({ line: 0, character: 0 });
	});
});

describe("computeChangeRange", () => {
	it("detects a single-line insertion", () => {
		const oldText = "hello world";
		const newText = "hello cruel world";
		const change = computeChangeRange(oldText, newText);
		expect(change.range.start).toEqual({ line: 0, character: 6 });
		expect(change.range.end).toEqual({ line: 0, character: 6 });
		expect(change.text).toBe("cruel ");
	});

	it("detects a single-line deletion", () => {
		const oldText = "hello cruel world";
		const newText = "hello world";
		const change = computeChangeRange(oldText, newText);
		expect(change.range.start).toEqual({ line: 0, character: 6 });
		expect(change.range.end).toEqual({ line: 0, character: 12 });
		expect(change.text).toBe("");
	});

	it("detects a single-line replacement", () => {
		const oldText = "hello world";
		const newText = "hello earth";
		const change = computeChangeRange(oldText, newText);
		expect(change.range.start).toEqual({ line: 0, character: 6 });
		expect(change.range.end).toEqual({ line: 0, character: 11 });
		expect(change.text).toBe("earth");
	});

	it("handles multi-line changes with correct line positions", () => {
		const oldText = "line1\nline2\nline3";
		const newText = "line1\nCHANGED\nline3";
		const change = computeChangeRange(oldText, newText);
		// Common prefix: "line1\n" → start at beginning of line 1
		expect(change.range.start).toEqual({ line: 1, character: 0 });
		// Common suffix: "\nline3" → end after "line2" on line 1
		expect(change.range.end).toEqual({ line: 1, character: 5 });
		expect(change.text).toBe("CHANGED");
	});

	it("handles insertion at end of file", () => {
		const oldText = "abc";
		const newText = "abcdef";
		const change = computeChangeRange(oldText, newText);
		expect(change.range.start).toEqual({ line: 0, character: 3 });
		expect(change.range.end).toEqual({ line: 0, character: 3 });
		expect(change.text).toBe("def");
	});

	it("handles complete file replacement (no common prefix/suffix)", () => {
		const oldText = "abc";
		const newText = "xyz";
		const change = computeChangeRange(oldText, newText);
		expect(change.range.start).toEqual({ line: 0, character: 0 });
		expect(change.range.end).toEqual({ line: 0, character: 3 });
		expect(change.text).toBe("xyz");
	});

	it("handles empty old text (new file)", () => {
		const oldText = "";
		const newText = "hello\nworld";
		const change = computeChangeRange(oldText, newText);
		expect(change.range.start).toEqual({ line: 0, character: 0 });
		expect(change.range.end).toEqual({ line: 0, character: 0 });
		expect(change.text).toBe("hello\nworld");
	});

	it("handles identical text (no change)", () => {
		const oldText = "same text";
		const newText = "same text";
		const change = computeChangeRange(oldText, newText);
		expect(change.range.start).toEqual({ line: 0, character: 9 });
		expect(change.range.end).toEqual({ line: 0, character: 9 });
		expect(change.text).toBe("");
	});

	it("handles change spanning multiple lines", () => {
		const oldText = "function foo() {\n  return 1;\n}\n";
		const newText = "function foo() {\n  return 2;\n  console.log('hi');\n}\n";
		const change = computeChangeRange(oldText, newText);
		// Common prefix: "function foo() {\n  return " (26 chars)
		// The change starts at "1" on line 1, character 9
		expect(change.range.start).toEqual({ line: 1, character: 9 });
		// Common suffix: ";\n}\n" (4 chars) → end at offset 27 (the ";")
		// offset 27 = line 1, char 10 (after "  return 1")
		expect(change.range.end).toEqual({ line: 1, character: 10 });
		expect(change.text).toBe("2;\n  console.log('hi')");
	});
});