Skip to content

Commit ba243a7

Browse files
feat(code-editor): add collapse unchanged lines extension with expand… (#1342)
Co-authored-by: Jonathan Mieloo <32547391+jonathanlab@users.noreply.github.com> Co-authored-by: JonathanLab <jonathanmieloo@gmail.com>
1 parent 0423c18 commit ba243a7

File tree

4 files changed

+555
-25
lines changed

4 files changed

+555
-25
lines changed
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { EditorState } from "@codemirror/state";
2+
import { describe, expect, it } from "vitest";
3+
import {
4+
applyExpandEffect,
5+
buildDecorations,
6+
type CollapsedRange,
7+
expandAll,
8+
expandDown,
9+
expandUp,
10+
mapPosBetweenSides,
11+
} from "./collapseUnchangedExtension";
12+
13+
function makeState(lineCount: number): EditorState {
14+
const lines = Array.from({ length: lineCount }, (_, i) => `line ${i + 1}`);
15+
return EditorState.create({ doc: lines.join("\n") });
16+
}
17+
18+
function range(
19+
from: number,
20+
to: number,
21+
limitFrom?: number,
22+
limitTo?: number,
23+
): CollapsedRange {
24+
return {
25+
fromLine: from,
26+
toLine: to,
27+
limitFromLine: limitFrom ?? from,
28+
limitToLine: limitTo ?? to,
29+
};
30+
}
31+
32+
describe("mapPosBetweenSides", () => {
33+
const chunks = [
34+
{ fromA: 10, toA: 20, fromB: 10, toB: 25 },
35+
{ fromA: 50, toA: 60, fromB: 55, toB: 70 },
36+
];
37+
38+
it("maps position before first chunk", () => {
39+
expect(mapPosBetweenSides(5, chunks, true)).toBe(5);
40+
expect(mapPosBetweenSides(5, chunks, false)).toBe(5);
41+
});
42+
43+
it("maps position between chunks from side A", () => {
44+
expect(mapPosBetweenSides(30, chunks, true)).toBe(35);
45+
});
46+
47+
it("maps position between chunks from side B", () => {
48+
expect(mapPosBetweenSides(35, chunks, false)).toBe(30);
49+
});
50+
51+
it("maps position after last chunk from side A", () => {
52+
expect(mapPosBetweenSides(80, chunks, true)).toBe(90);
53+
});
54+
55+
it("handles empty chunks array", () => {
56+
expect(mapPosBetweenSides(42, [], true)).toBe(42);
57+
expect(mapPosBetweenSides(42, [], false)).toBe(42);
58+
});
59+
60+
it("maps position at exact chunk boundary", () => {
61+
expect(mapPosBetweenSides(10, chunks, true)).toBe(10);
62+
});
63+
});
64+
65+
describe("applyExpandEffect", () => {
66+
const state = makeState(20);
67+
68+
const ranges: CollapsedRange[] = [range(1, 5), range(12, 18)];
69+
70+
it("expandAll removes the targeted range", () => {
71+
const pos = state.doc.line(3).from;
72+
const effect = expandAll.of(pos);
73+
const result = applyExpandEffect(ranges, state, effect);
74+
75+
expect(result).toEqual([range(12, 18)]);
76+
});
77+
78+
it("expandAll leaves non-targeted ranges intact", () => {
79+
const pos = state.doc.line(8).from;
80+
const effect = expandAll.of(pos);
81+
const result = applyExpandEffect(ranges, state, effect);
82+
83+
expect(result).toEqual(ranges);
84+
});
85+
86+
it("expandUp reveals lines above the collapsed range", () => {
87+
const pos = state.doc.line(14).from;
88+
const effect = expandUp.of({ pos, lines: 3 });
89+
const result = applyExpandEffect(ranges, state, effect);
90+
91+
expect(result).toEqual([range(1, 5), range(15, 18, 12, 18)]);
92+
});
93+
94+
it("expandDown reveals lines below the collapsed range", () => {
95+
const pos = state.doc.line(14).from;
96+
const effect = expandDown.of({ pos, lines: 3 });
97+
const result = applyExpandEffect(ranges, state, effect);
98+
99+
expect(result).toEqual([range(1, 5), range(12, 15, 12, 18)]);
100+
});
101+
102+
it("expandUp removes range when lines exceed range size", () => {
103+
const pos = state.doc.line(3).from;
104+
const effect = expandUp.of({ pos, lines: 100 });
105+
const result = applyExpandEffect(ranges, state, effect);
106+
107+
expect(result).toEqual([range(12, 18)]);
108+
});
109+
110+
it("expandDown removes range when lines exceed range size", () => {
111+
const pos = state.doc.line(3).from;
112+
const effect = expandDown.of({ pos, lines: 100 });
113+
const result = applyExpandEffect(ranges, state, effect);
114+
115+
expect(result).toEqual([range(12, 18)]);
116+
});
117+
118+
it("preserves original boundaries through multiple expansions", () => {
119+
const pos = state.doc.line(14).from;
120+
const first = applyExpandEffect(
121+
ranges,
122+
state,
123+
expandUp.of({ pos, lines: 2 }),
124+
);
125+
const second = applyExpandEffect(
126+
first,
127+
state,
128+
expandDown.of({ pos: state.doc.line(16).from, lines: 2 }),
129+
);
130+
131+
expect(second).toEqual([range(1, 5), range(14, 16, 12, 18)]);
132+
});
133+
});
134+
135+
describe("buildDecorations", () => {
136+
it("skips ranges where fromLine > toLine", () => {
137+
const state = makeState(10);
138+
const ranges: CollapsedRange[] = [range(5, 3)];
139+
const deco = buildDecorations(state, ranges);
140+
141+
expect(deco.size).toBe(0);
142+
});
143+
144+
it("creates decorations for valid ranges", () => {
145+
const state = makeState(20);
146+
const ranges: CollapsedRange[] = [range(3, 7), range(15, 18)];
147+
const deco = buildDecorations(state, ranges);
148+
149+
expect(deco.size).toBe(2);
150+
});
151+
152+
it("handles empty ranges array", () => {
153+
const state = makeState(10);
154+
const deco = buildDecorations(state, []);
155+
156+
expect(deco.size).toBe(0);
157+
});
158+
159+
it("creates single-line range decoration", () => {
160+
const state = makeState(10);
161+
const ranges: CollapsedRange[] = [range(5, 5)];
162+
const deco = buildDecorations(state, ranges);
163+
164+
expect(deco.size).toBe(1);
165+
});
166+
});

0 commit comments

Comments
 (0)