@cueloop/client 0.1.0-alpha.91 → 0.1.0-alpha.93

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/package.json +5 -5
  2. package/src/App.deleted-scroll.test.tsx +186 -0
  3. package/src/App.diff.test.tsx +3 -4
  4. package/src/App.test.tsx +1 -0
  5. package/src/App.toast.test.tsx +27 -1
  6. package/src/App.tsx +21 -12
  7. package/src/App.workbench.test.tsx +208 -2
  8. package/src/app-screens.tsx +34 -8
  9. package/src/components/AnnotatableFileView.test.tsx +48 -1
  10. package/src/components/AppShell.zoom.test.tsx +16 -3
  11. package/src/components/ChangesColumn.tsx +10 -4
  12. package/src/components/ChangesFileTree.test.tsx +122 -4
  13. package/src/components/DiffContentView.scroll.test.tsx +285 -1
  14. package/src/components/DiffContentView.tsx +214 -56
  15. package/src/components/EditorGrid.split.test.tsx +118 -0
  16. package/src/components/EditorGrid.stories.tsx +27 -0
  17. package/src/components/EditorGrid.tsx +170 -50
  18. package/src/components/EditorGrid.zoom.test.tsx +65 -1
  19. package/src/components/GridTabContent.actions.test.tsx +151 -0
  20. package/src/components/GridTabContent.stories.tsx +2 -2
  21. package/src/components/GridTabContent.tsx +5 -1
  22. package/src/components/ProjectTreeView.test.tsx +166 -8
  23. package/src/components/ProjectTreeView.tsx +53 -27
  24. package/src/components/ScrollArea.tsx +21 -2
  25. package/src/components/Toast.tsx +3 -1
  26. package/src/components/WelcomePlayground.test.tsx +28 -1
  27. package/src/components/__snapshots__/stories.test.tsx.snap +41 -21
  28. package/src/components/editor-grid.test.ts +87 -0
  29. package/src/components/editor-grid.ts +142 -6
  30. package/src/components/primitives/Tree.tsx +51 -15
  31. package/src/diff-syntax.test.ts +15 -2
  32. package/src/diff-syntax.ts +13 -5
  33. package/src/thread-controller.curate.test.ts +27 -0
  34. package/src/thread-controller.ts +4 -6
  35. package/src/use-annotation-surface.tsx +86 -19
  36. package/src/use-changes-workbench.ts +30 -18
  37. package/src/use-frame-measure.ts +40 -7
  38. package/src/use-settings-dialog.tsx +1 -13
  39. package/src/use-shared-keyboard.ts +55 -0
  40. package/src/use-terminal-virtualizer.ts +8 -2
@@ -107,9 +107,24 @@ test("walking the caret down past wrapped discussion cards keeps it on screen an
107
107
  await press(setup, "down");
108
108
  }
109
109
 
110
- // Assert - scrollTop only ever grows going down (never rebounds/jitters)
110
+ // A key advances the viewport by at most one visual line, even across a tall card.
111
111
  for (let step = 1; step < scrollTops.length; step++) {
112
112
  expect(scrollTops[step]!).toBeGreaterThanOrEqual(scrollTops[step - 1]!);
113
+ expect(scrollTops[step]! - scrollTops[step - 1]!).toBeLessThanOrEqual(1);
114
+ }
115
+
116
+ // Walk back through the same cards: upward reveal must also stay one row at a time.
117
+ const upwardTops: number[] = [];
118
+
119
+ for (let step = 0; step < codeRowCount + 20; step++) {
120
+ upwardTops.push(scrollbox.scrollTop);
121
+ screenRows.push(caretScreenRow());
122
+ await press(setup, "up");
123
+ }
124
+
125
+ for (let step = 1; step < upwardTops.length; step++) {
126
+ expect(upwardTops[step]!).toBeLessThanOrEqual(upwardTops[step - 1]!);
127
+ expect(upwardTops[step - 1]! - upwardTops[step]!).toBeLessThanOrEqual(1);
113
128
  }
114
129
 
115
130
  // Assert - the caret never scrolls off screen while it walks past each wrapped card
@@ -120,6 +135,220 @@ test("walking the caret down past wrapped discussion cards keeps it on screen an
120
135
  setup.renderer.destroy();
121
136
  }, 25000);
122
137
 
138
+ test("wheel input accelerates a continuous gesture and settles back to precise steps", async () => {
139
+ const rows = diffRows(tallPatch(80));
140
+ const setup = await testRender(
141
+ <DiffContentView
142
+ rows={rows}
143
+ session={fixtureDiffSession()}
144
+ marks={marksByRows([], rows)}
145
+ quickActions={[]}
146
+ observer={false}
147
+ onAnnotate={noop}
148
+ onReply={noop}
149
+ onUpdateAnnotation={noop}
150
+ onExit={noop}
151
+ />,
152
+ { width: 60, height: 12 },
153
+ );
154
+
155
+ await settle(setup);
156
+ const found = findById(setup.renderer.root, "diff-scroll");
157
+
158
+ if (!(found instanceof ScrollBoxRenderable)) throw new Error("diff-scroll is not a scrollbox");
159
+ const scrollbox = found;
160
+ const acceleration = scrollbox.scrollAcceleration;
161
+ const first = acceleration.tick(1000);
162
+ const burst = Array.from({ length: 10 }, (_, index) => acceleration.tick(1016 + index * 16));
163
+ const tail = acceleration.tick(1400);
164
+
165
+ expect(scrollbox.verticalScrollBar.scrollStep).toBe(1);
166
+ expect(first).toBe(1);
167
+ expect(Math.max(...burst)).toBeGreaterThan(first);
168
+ expect(tail).toBe(1);
169
+
170
+ acceleration.reset();
171
+ await setup.mockMouse.scroll(20, 5, "down");
172
+ await settle(setup);
173
+ expect(scrollbox.scrollTop).toBeGreaterThan(0);
174
+
175
+ setup.renderer.destroy();
176
+ });
177
+
178
+ test("wheel scrolling up keeps the caret on the bottom visible code row", async () => {
179
+ const rows = diffRows(tallPatch(80));
180
+ const setup = await testRender(
181
+ <DiffContentView
182
+ rows={rows}
183
+ session={fixtureDiffSession()}
184
+ marks={marksByRows([], rows)}
185
+ quickActions={[]}
186
+ observer={false}
187
+ onAnnotate={noop}
188
+ onReply={noop}
189
+ onUpdateAnnotation={noop}
190
+ onExit={noop}
191
+ />,
192
+ { width: 60, height: 12 },
193
+ );
194
+
195
+ await settle(setup);
196
+ const found = findById(setup.renderer.root, "diff-scroll");
197
+
198
+ if (!(found instanceof ScrollBoxRenderable)) throw new Error("diff-scroll is not a scrollbox");
199
+ const scrollbox = found;
200
+
201
+ for (const key of Array.from({ length: 30 }, () => "down" as const)) {
202
+ // eslint-disable-next-line no-await-in-loop
203
+ await press(setup, key);
204
+ }
205
+ const before = scrollbox.scrollTop;
206
+ const markerRows = (): number[] =>
207
+ setup
208
+ .captureCharFrame()
209
+ .split("\n")
210
+ .flatMap((line, row) => (line.includes("▎") ? [row] : []));
211
+ const top = scrollbox.viewport.screenY;
212
+ const bottom = top + scrollbox.viewport.height - 1;
213
+
214
+ for (const direction of Array.from({ length: 8 }, () => "up" as const)) {
215
+ // eslint-disable-next-line no-await-in-loop
216
+ await setup.mockMouse.scroll(20, 5, direction);
217
+ // eslint-disable-next-line no-await-in-loop
218
+ await settle(setup);
219
+ expect(markerRows()).toEqual([bottom]);
220
+ }
221
+ expect(scrollbox.scrollTop).toBeLessThan(before);
222
+ for (const direction of Array.from({ length: 20 }, () => "down" as const)) {
223
+ // eslint-disable-next-line no-await-in-loop
224
+ await setup.mockMouse.scroll(20, 5, direction);
225
+ // eslint-disable-next-line no-await-in-loop
226
+ await settle(setup);
227
+ expect(markerRows()).toHaveLength(1);
228
+ }
229
+ expect(markerRows()).toEqual([top]);
230
+ setup.renderer.destroy();
231
+ });
232
+
233
+ test("rapid wheel scrolling through a large wrapped diff does not loop React updates", async () => {
234
+ const filePatches = Array.from({ length: 80 }, (_, fileIndex) => [
235
+ `diff --git a/file-${fileIndex}.ts b/file-${fileIndex}.ts`,
236
+ `--- a/file-${fileIndex}.ts`,
237
+ `+++ b/file-${fileIndex}.ts`,
238
+ "@@ -0,0 +1,12 @@",
239
+ ...Array.from(
240
+ { length: 12 },
241
+ (_, lineIndex) => `+line ${lineIndex} ${"dense source text ".repeat(80)}`,
242
+ ),
243
+ ]).flat();
244
+ const rows = diffRows([...filePatches, ""].join("\n"));
245
+ const StatefulDiff = (): React.ReactNode => {
246
+ const [, setCursor] = React.useState(0);
247
+
248
+ return (
249
+ <DiffContentView
250
+ rows={rows}
251
+ session={fixtureDiffSession()}
252
+ marks={marksByRows([], rows)}
253
+ quickActions={[]}
254
+ observer={false}
255
+ onCursorChange={setCursor}
256
+ onAnnotate={noop}
257
+ onReply={noop}
258
+ onUpdateAnnotation={noop}
259
+ onExit={noop}
260
+ />
261
+ );
262
+ };
263
+ const setup = await testRender(<StatefulDiff />, { width: 60, height: 12 });
264
+
265
+ await settle(setup);
266
+ const found = findById(setup.renderer.root, "diff-scroll");
267
+
268
+ if (!(found instanceof ScrollBoxRenderable)) throw new Error("diff-scroll is not a scrollbox");
269
+
270
+ for (const [index, direction] of Array.from({ length: 240 }, () => "down" as const).entries()) {
271
+ // eslint-disable-next-line no-await-in-loop
272
+ await setup.mockMouse.scroll(20, 5, direction);
273
+ if (index % 12 === 0) {
274
+ // eslint-disable-next-line no-await-in-loop
275
+ await settle(setup);
276
+ }
277
+ }
278
+ await settle(setup);
279
+ expect(found.scrollTop).toBeGreaterThan(0);
280
+ setup.renderer.destroy();
281
+ }, 60000);
282
+
283
+ test("walking long wrapped code lines advances the viewport one visual row per key", async () => {
284
+ const lines = Array.from(
285
+ { length: 36 },
286
+ (_, index) =>
287
+ `+line ${String(index).padStart(2, "0")} ${"a paragraph with several wrapped phrases ".repeat(5)}`,
288
+ );
289
+ const rows = diffRows(
290
+ [
291
+ "diff --git a/long.md b/long.md",
292
+ "--- a/long.md",
293
+ "+++ b/long.md",
294
+ "@@ -0,0 +1,36 @@",
295
+ ...lines,
296
+ "",
297
+ ].join("\n"),
298
+ );
299
+ const setup = await testRender(
300
+ <DiffContentView
301
+ rows={rows}
302
+ session={fixtureDiffSession()}
303
+ marks={marksByRows([], rows)}
304
+ quickActions={[]}
305
+ observer={false}
306
+ onAnnotate={noop}
307
+ onReply={noop}
308
+ onUpdateAnnotation={noop}
309
+ onExit={noop}
310
+ />,
311
+ { width: 60, height: 12 },
312
+ );
313
+
314
+ await settle(setup);
315
+ const found = findById(setup.renderer.root, "diff-scroll");
316
+
317
+ if (!(found instanceof ScrollBoxRenderable)) throw new Error("diff-scroll is not a scrollbox");
318
+ const scrollbox = found;
319
+ const steps: number[] = [];
320
+ const caretCell = annotationPaletteFor(DARK).caretCell;
321
+
322
+ for (let index = 0; index < 45; index++) {
323
+ const before = scrollbox.scrollTop;
324
+
325
+ await press(setup, "down");
326
+ steps.push(scrollbox.scrollTop - before);
327
+ const caretRow = setup
328
+ .captureSpans()
329
+ .lines.findIndex((line) => line.spans.some((span) => hex(span.bg) === caretCell));
330
+ const markerRows = setup
331
+ .captureCharFrame()
332
+ .split("\n")
333
+ .flatMap((line, row) => (line.includes("▎") ? [row] : []));
334
+
335
+ expect(caretRow).toBeGreaterThanOrEqual(0);
336
+ expect(markerRows).toEqual([caretRow]);
337
+ }
338
+
339
+ expect(Math.max(...steps)).toBeLessThanOrEqual(1);
340
+ expect(steps.some((step) => step > 0)).toBe(true);
341
+ await setup.mockMouse.scroll(20, 5, "up");
342
+ await settle(setup);
343
+ const wrappedMarkerRows = setup
344
+ .captureCharFrame()
345
+ .split("\n")
346
+ .flatMap((line, row) => (line.includes("▎") ? [row] : []));
347
+
348
+ expect(wrappedMarkerRows).toEqual([scrollbox.viewport.screenY + scrollbox.viewport.height - 1]);
349
+ setup.renderer.destroy();
350
+ });
351
+
123
352
  test("opening a comment on the diff's last line reveals its composer", async () => {
124
353
  const rows = diffRows(tallPatch(24));
125
354
  const setup = await testRender(
@@ -147,3 +376,58 @@ test("opening a comment on the diff's last line reveals its composer", async ()
147
376
  expect(setup.captureCharFrame()).toContain("● bottom comment");
148
377
  setup.renderer.destroy();
149
378
  }, 25000);
379
+
380
+ test("Up keeps the caret on the first screen row while wrapped lines scroll", async () => {
381
+ const lines = Array.from({ length: 24 }, (_, index) =>
382
+ index % 3 === 2 ? "-" : `-line ${index} ${"wrapped words here ".repeat(5)}`,
383
+ );
384
+ const rows = diffRows(
385
+ [
386
+ "diff --git a/long.md b/long.md",
387
+ "--- a/long.md",
388
+ "+++ /dev/null",
389
+ "@@ -1,24 +0,0 @@",
390
+ ...lines,
391
+ "",
392
+ ].join("\n"),
393
+ );
394
+ const setup = await testRender(
395
+ <DiffContentView
396
+ rows={rows}
397
+ session={fixtureDiffSession()}
398
+ marks={marksByRows([], rows)}
399
+ quickActions={[]}
400
+ observer={false}
401
+ onAnnotate={noop}
402
+ onReply={noop}
403
+ onUpdateAnnotation={noop}
404
+ onExit={noop}
405
+ />,
406
+ { width: 60, height: 12 },
407
+ );
408
+ await settle(setup);
409
+ const found = findById(setup.renderer.root, "diff-scroll");
410
+
411
+ if (!(found instanceof ScrollBoxRenderable)) throw new Error("diff-scroll is not a scrollbox");
412
+ const scrollbox = found;
413
+
414
+ for (let index = 0; index < 40; index++) await press(setup, "down");
415
+ let previousTop = scrollbox.scrollTop;
416
+ let scrolled = false;
417
+
418
+ for (let index = 0; index < 80; index++) {
419
+ await press(setup, "up");
420
+ const frame = setup.captureCharFrame().split("\n");
421
+ const marker = frame.findIndex((line) => line.includes("▎"));
422
+ const top = scrollbox.scrollTop;
423
+
424
+ expect(previousTop - top).toBeLessThanOrEqual(1);
425
+ if (top < previousTop) {
426
+ expect(marker).toBe(0);
427
+ scrolled = true;
428
+ }
429
+ previousTop = top;
430
+ }
431
+ expect(scrolled).toBe(true);
432
+ setup.renderer.destroy();
433
+ });