summaryrefslogtreecommitdiffhomepage
path: root/packages/lsp/src/diff.test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/lsp/src/diff.test.ts')
-rw-r--r--packages/lsp/src/diff.test.ts117
1 files changed, 117 insertions, 0 deletions
diff --git a/packages/lsp/src/diff.test.ts b/packages/lsp/src/diff.test.ts
new file mode 100644
index 0000000..b5b6a7b
--- /dev/null
+++ b/packages/lsp/src/diff.test.ts
@@ -0,0 +1,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')");
+ });
+});