Skip to content

Commit 3a107bd

Browse files
committed
Handle selection & scroll pos in popstate navigation
1 parent c9ebf4d commit 3a107bd

File tree

7 files changed

+149
-118
lines changed

7 files changed

+149
-118
lines changed

bun.lock

Lines changed: 42 additions & 88 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web-extension/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,17 @@
1818
"devDependencies": {
1919
"@eslint/compat": "^2.0.0",
2020
"@eslint/js": "^9.39.1",
21-
"@types/bun": "^1.3.2",
21+
"@types/bun": "^1.3.3",
2222
"@types/webextension-polyfill": "^0.12.4",
23-
"chrome-types": "^0.1.387",
23+
"chrome-types": "^0.1.390",
2424
"eslint": "^9.39.1",
2525
"eslint-config-prettier": "^10.1.8",
2626
"globals": "^16.5.0",
2727
"prettier": "^3.6.2",
2828
"prettier-plugin-tailwindcss": "^0.7.1",
2929
"typescript": "^5.9.3",
30-
"typescript-eslint": "^8.46.3",
31-
"vite": "^7.2.2"
30+
"typescript-eslint": "^8.48.0",
31+
"vite": "^7.2.4"
3232
},
3333
"dependencies": {
3434
"@tailwindcss/vite": "^4.1.17",

web/package.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@
1919
"devDependencies": {
2020
"@eslint/compat": "^2.0.0",
2121
"@eslint/js": "^9.39.1",
22-
"@iconify-json/octicon": "^1.2.17",
22+
"@iconify-json/octicon": "^1.2.19",
2323
"@iconify/tailwind4": "^1.1.0",
2424
"@octokit/openapi-types": "^27.0.0",
2525
"@sveltejs/adapter-auto": "^7.0.0",
2626
"@sveltejs/adapter-cloudflare": "^7.2.4",
27-
"@sveltejs/kit": "^2.48.4",
27+
"@sveltejs/kit": "^2.49.0",
2828
"@sveltejs/vite-plugin-svelte": "^6.2.1",
2929
"@tailwindcss/vite": "^4.1.17",
3030
"@types/chroma-js": "^3.1.2",
@@ -37,18 +37,18 @@
3737
"prettier": "^3.6.2",
3838
"prettier-plugin-svelte": "^3.4.0",
3939
"prettier-plugin-tailwindcss": "^0.7.1",
40-
"svelte": "^5.43.4",
40+
"svelte": "^5.43.14",
4141
"svelte-adapter-bun": "^1.0.1",
42-
"svelte-check": "^4.3.3",
42+
"svelte-check": "^4.3.4",
4343
"tailwindcss": "^4.1.17",
4444
"tw-animate-css": "^1.4.0",
4545
"typescript": "^5.9.3",
46-
"typescript-eslint": "^8.46.3",
47-
"vite": "^7.2.2",
48-
"vitest": "^4.0.8"
46+
"typescript-eslint": "^8.48.0",
47+
"vite": "^7.2.4",
48+
"vitest": "^4.0.13"
4949
},
5050
"dependencies": {
51-
"bits-ui": "^2.14.2",
51+
"bits-ui": "^2.14.4",
5252
"chroma-js": "^3.1.2",
5353
"diff": "^8.0.2",
5454
"luxon": "^3.7.2",

web/src/app.d.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,14 @@ declare global {
55
// interface Error {}
66
// interface Locals {}
77
// interface PageData {}
8-
// interface PageState {}
8+
interface PageState {
9+
scrollOffset?: number;
10+
selection?: {
11+
fileIdx: number;
12+
lines?: LineSelection;
13+
unresolvedLines?: UnresolvedLineSelection;
14+
};
15+
}
916
// interface Platform {}
1017
}
1118
}

web/src/lib/components/diff/concise-diff-view.svelte.ts

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1260,24 +1260,19 @@ export class ConciseDiffViewState<K> {
12601260
return;
12611261
}
12621262

1263-
const destroyClick = on(element, "click", (e) => {
1264-
// Only handle click if we didn't just finish dragging
1265-
if (this.suppressNextClick) {
1266-
this.suppressNextClick = false;
1267-
return;
1268-
}
1269-
this.updateSelection(hunk, hunkIdx, line, lineIdx, e.shiftKey);
1270-
});
1271-
12721263
const destroyPointerDown = on(element, "pointerdown", (e: PointerEvent) => {
1273-
// Only start drag on left click without shift key
1274-
if (e.button === 0 && !e.shiftKey) {
1264+
if (e.button !== 0) return; // only handle left click
1265+
1266+
if (e.shiftKey) {
1267+
// Handle shift+click for adjusting selection
1268+
this.updateSelection(hunk, hunkIdx, line, lineIdx, true);
1269+
} else {
1270+
// Handle regular click with drag support
12751271
this.startDrag(element, e.pointerId, hunk, hunkIdx, line, lineIdx);
12761272
}
12771273
});
12781274

12791275
return () => {
1280-
destroyClick();
12811276
destroyPointerDown();
12821277
};
12831278
};

web/src/lib/diff-viewer.svelte.ts

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ import { ProgressBarState } from "$lib/components/progress-bar/index.svelte";
2929
import { Keybinds } from "./keybinds.svelte";
3030
import { LayoutState, type PersistentLayoutState } from "./layout.svelte";
3131
import { page } from "$app/state";
32-
import { goto } from "$app/navigation";
32+
import { afterNavigate, goto } from "$app/navigation";
33+
import { type AfterNavigate } from "@sveltejs/kit";
3334

3435
export const GITHUB_URL_PARAM = "github_url";
3536
export const PATCH_URL_PARAM = "patch_url";
@@ -320,12 +321,43 @@ export class MultiFileDiffViewerState {
320321
}
321322
});
322323

324+
afterNavigate((nav) => {
325+
this.afterNavigate(nav);
326+
});
327+
328+
this.registerKeybinds();
329+
}
330+
331+
private registerKeybinds() {
323332
const keybinds = new Keybinds();
324333
keybinds.registerModifierBind("o", () => this.openOpenDiffDialog());
325334
keybinds.registerModifierBind(",", () => this.openSettingsDialog());
326335
keybinds.registerModifierBind("b", () => this.layoutState.toggleSidebar());
327336
}
328337

338+
private afterNavigate(nav: AfterNavigate) {
339+
if (!this.vlist) return;
340+
341+
if (nav.type === "popstate") {
342+
const selection = page.state.selection;
343+
const file = selection ? this.fileDetails[selection.fileIdx] : undefined;
344+
if (selection && file) {
345+
this.selection = {
346+
file,
347+
lines: selection.lines,
348+
unresolvedLines: selection.unresolvedLines,
349+
};
350+
} else {
351+
this.selection = undefined;
352+
}
353+
354+
const scrollOffset = page.state.scrollOffset;
355+
if (scrollOffset !== undefined) {
356+
this.vlist.scrollTo(scrollOffset);
357+
}
358+
}
359+
}
360+
329361
openOpenDiffDialog() {
330362
this.openDiffDialogOpen = true;
331363
this.settingsDialogOpen = false;
@@ -378,11 +410,25 @@ export class MultiFileDiffViewerState {
378410
return null;
379411
}
380412

413+
private createPageState(): App.PageState {
414+
return {
415+
scrollOffset: this.vlist?.getScrollOffset(),
416+
selection: this.selection
417+
? {
418+
fileIdx: this.selection.file.index,
419+
lines: $state.snapshot(this.selection.lines),
420+
unresolvedLines: $state.snapshot(this.selection.unresolvedLines),
421+
}
422+
: undefined,
423+
};
424+
}
425+
381426
setSelection(file: FileDetails, lines: LineSelection | undefined) {
382427
this.selection = { file, lines };
383428

384429
goto(`?${page.url.searchParams}#${makeUrlHashValue(this.selection)}`, {
385430
keepFocus: true,
431+
state: this.createPageState(),
386432
});
387433
}
388434

@@ -391,6 +437,7 @@ export class MultiFileDiffViewerState {
391437

392438
goto(`?${page.url.searchParams}`, {
393439
keepFocus: true,
440+
state: this.createPageState(),
394441
});
395442
}
396443

@@ -567,12 +614,14 @@ export class MultiFileDiffViewerState {
567614
file,
568615
unresolvedLines: urlSelection.lines,
569616
};
570-
await goto(`?${page.url.searchParams}#${makeUrlHashValue(this.selection)}`, {
571-
keepFocus: true,
572-
});
573617
this.scrollToFile(file.index, {
574618
focus: !urlSelection.lines,
575619
});
620+
await animationFramePromise();
621+
await goto(`?${page.url.searchParams}#${makeUrlHashValue(this.selection)}`, {
622+
keepFocus: true,
623+
state: this.createPageState(),
624+
});
576625
} else {
577626
await goto(`?${page.url.searchParams}`, {
578627
keepFocus: true,

web/src/routes/FileHeader.svelte

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import { type FileDetails, MultiFileDiffViewerState } from "$lib/diff-viewer.svelte";
55
import { GlobalOptions } from "$lib/global-options.svelte";
66
import { Popover, Button } from "bits-ui";
7+
import { boolAttr } from "runed";
78
import { tick } from "svelte";
89
910
interface Props {
@@ -48,6 +49,11 @@
4849
viewer.scrollToFile(index, { autoExpand: false, smooth: true });
4950
viewer.setSelection(value, undefined);
5051
}
52+
53+
let selected = $derived.by(() => {
54+
const sel = viewer.getSelection(value);
55+
return sel && sel.lines === undefined && sel.unresolvedLines === undefined;
56+
});
5157
</script>
5258

5359
{#snippet fileName()}
@@ -116,11 +122,15 @@
116122

117123
<div
118124
id="file-header-{index}"
119-
class="sticky top-0 z-10 flex flex-row items-center gap-2 border-b bg-neutral px-2 py-1 text-sm shadow-sm focus:ring-2 focus:ring-primary focus:outline-none focus:ring-inset"
125+
class={[
126+
"sticky top-0 z-10 flex flex-row items-center gap-2 border-b bg-neutral px-2 py-1 text-sm shadow-sm",
127+
"focus-and-selected-styles focus:outline-none",
128+
]}
120129
tabindex={0}
121130
role="button"
122131
onclick={() => selectHeader()}
123132
onkeyup={(event) => event.key === "Enter" && selectHeader()}
133+
data-selected={boolAttr(selected)}
124134
>
125135
{#if value.type === "text"}
126136
<DiffStats brief add={viewer.stats.fileAddedLines[index]} remove={viewer.stats.fileRemovedLines[index]} />
@@ -136,3 +146,19 @@
136146
{/if}
137147
</div>
138148
</div>
149+
150+
<style>
151+
.focus-and-selected-styles {
152+
&:focus {
153+
box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--color-primary) 50%, transparent);
154+
}
155+
&[data-selected] {
156+
box-shadow: inset 4px 0 0 0 var(--color-primary);
157+
}
158+
&:focus[data-selected] {
159+
box-shadow:
160+
inset 0 0 0 2px color-mix(in srgb, var(--color-primary) 50%, transparent),
161+
inset 4px 0 0 0 var(--color-primary);
162+
}
163+
}
164+
</style>

0 commit comments

Comments
 (0)