|
2 | 2 | import { |
3 | 3 | type ConciseDiffViewProps, |
4 | 4 | ConciseDiffViewState, |
| 5 | + type DiffViewerPatchHunk, |
5 | 6 | innerPatchLineTypeProps, |
6 | 7 | type InnerPatchLineTypeProps, |
7 | 8 | makeSearchSegments, |
|
13 | 14 | type SearchSegment, |
14 | 15 | } from "$lib/components/diff/concise-diff-view.svelte"; |
15 | 16 | import Spinner from "$lib/components/Spinner.svelte"; |
16 | | - import { onDestroy } from "svelte"; |
| 17 | + import { onMount } from "svelte"; |
17 | 18 | import { type MutableValue } from "$lib/util"; |
18 | 19 | import { box } from "svelte-toolbelt"; |
| 20 | + import { boolAttr } from "runed"; |
19 | 21 |
|
20 | 22 | let { |
21 | 23 | rawPatchContent, |
|
28 | 30 | searchQuery, |
29 | 31 | searchMatchingLines, |
30 | 32 | activeSearchResult = -1, |
| 33 | + jumpToSearchResult = $bindable(false), |
31 | 34 | cache, |
32 | 35 | cacheKey, |
| 36 | + unresolvedSelection, |
| 37 | + selection = $bindable(), |
| 38 | + jumpToSelection = $bindable(false), |
33 | 39 | }: ConciseDiffViewProps<K> = $props(); |
34 | 40 |
|
35 | 41 | const parsedPatch = $derived.by(() => { |
|
48 | 54 | omitPatchHeaderOnlyHunks: box.with(() => omitPatchHeaderOnlyHunks), |
49 | 55 | wordDiffs: box.with(() => wordDiffs), |
50 | 56 |
|
| 57 | + unresolvedSelection: box.with(() => unresolvedSelection), |
| 58 | + selection: box.with( |
| 59 | + () => selection, |
| 60 | + (v) => (selection = v), |
| 61 | + ), |
| 62 | +
|
51 | 63 | cache: box.with(() => cache), |
52 | 64 | cacheKey: box.with(() => cacheKey), |
53 | 65 | }); |
|
60 | 72 | } |
61 | 73 | } |
62 | 74 |
|
63 | | - let searchResultElements: HTMLSpanElement[] = $state([]); |
64 | | - let didInitialJump = $state(false); |
65 | | - let scheduledJump: ReturnType<typeof setTimeout> | undefined = undefined; |
66 | | - $effect(() => { |
67 | | - if (didInitialJump) { |
68 | | - return; |
69 | | - } |
70 | | - if (activeSearchResult >= 0 && searchResultElements[activeSearchResult] !== undefined) { |
71 | | - const element = searchResultElements[activeSearchResult]; |
72 | | - const anchorElement = element.closest("tr"); |
73 | | - // This is an exceptionally stupid and unreliable hack, but at least |
74 | | - // jumping to a result in a not-yet-loaded file works most of the time with a delay |
75 | | - // instead of never. |
76 | | - scheduledJump = setTimeout(() => { |
77 | | - if (scheduledJump !== undefined) { |
78 | | - clearTimeout(scheduledJump); |
79 | | - scheduledJump = undefined; |
80 | | - } |
81 | | -
|
82 | | - if (anchorElement !== null) { |
83 | | - anchorElement.scrollIntoView({ block: "center", inline: "center" }); |
84 | | - } |
85 | | - }, 200); |
86 | | - didInitialJump = true; |
87 | | - } |
88 | | - }); |
89 | | - onDestroy(() => { |
90 | | - if (scheduledJump !== undefined) { |
91 | | - clearTimeout(scheduledJump); |
92 | | - scheduledJump = undefined; |
93 | | - } |
94 | | - }); |
95 | | -
|
96 | 75 | let searchSegments: Promise<SearchSegment[][][]> = $derived.by(async () => { |
97 | 76 | if (!searchQuery || !searchMatchingLines) { |
98 | 77 | return []; |
|
134 | 113 | } |
135 | 114 | return segments; |
136 | 115 | }); |
| 116 | +
|
| 117 | + let selectionMidpoint = $derived.by(() => { |
| 118 | + if (!selection) return null; |
| 119 | + const startIdx = selection.start.idx; |
| 120 | + const endIdx = selection.end.idx; |
| 121 | + return Math.floor((startIdx + endIdx) / 2); |
| 122 | + }); |
137 | 123 | </script> |
138 | 124 |
|
139 | 125 | {#snippet lineContent(line: PatchLine, lineType: PatchLineTypeProps, innerLineType: InnerPatchLineTypeProps)} |
|
165 | 151 | <span class="inline leading-[0.875rem]"> |
166 | 152 | {#each lineSearchSegments as searchSegment, index (index)} |
167 | 153 | {#if searchSegment.highlighted}<span |
168 | | - bind:this={searchResultElements[searchSegment.id ?? -1]} |
| 154 | + {@attach (element) => { |
| 155 | + onMount(() => { |
| 156 | + if (jumpToSearchResult && searchSegment.id === activeSearchResult) { |
| 157 | + jumpToSearchResult = false; |
| 158 | + // See similar code & comment below around jumping to selections |
| 159 | + const scheduledJump = setTimeout(() => { |
| 160 | + element.scrollIntoView({ block: "center", inline: "center" }); |
| 161 | + }, 100); |
| 162 | + return () => { |
| 163 | + clearTimeout(scheduledJump); |
| 164 | + }; |
| 165 | + } |
| 166 | + }); |
| 167 | + }} |
169 | 168 | class={{ |
170 | 169 | "bg-[#d4a72c66]": searchSegment.id !== activeSearchResult, |
171 | 170 | "bg-[#ff9632]": searchSegment.id === activeSearchResult, |
|
186 | 185 | {/await} |
187 | 186 | {/snippet} |
188 | 187 |
|
189 | | -{#snippet renderLine(line: PatchLine, hunkIndex: number, lineIndex: number)} |
| 188 | +{#snippet renderLine(line: PatchLine, hunk: DiffViewerPatchHunk, hunkIndex: number, lineIndex: number)} |
190 | 189 | {@const lineType = patchLineTypeProps[line.type]} |
191 | | - <div class="bg-[var(--hunk-header-bg)]"> |
| 190 | + <div class="bg-[var(--hunk-header-bg)]" {@attach view.selectable(hunk, hunkIndex, line, lineIndex)}> |
192 | 191 | <div class="line-number h-full px-2 select-none {lineType.lineNoClasses}">{getDisplayLineNo(line, line.oldLineNo)}</div> |
193 | 192 | </div> |
194 | | - <div class="bg-[var(--hunk-header-bg)]"> |
195 | | - <div class="line-number h-full px-2 select-none {lineType.lineNoClasses}">{getDisplayLineNo(line, line.newLineNo)}</div> |
| 193 | + <div class="bg-[var(--hunk-header-bg)]" {@attach view.selectable(hunk, hunkIndex, line, lineIndex)}> |
| 194 | + <div |
| 195 | + class="selected-indicator line-number h-full px-2 select-none {lineType.lineNoClasses}" |
| 196 | + data-selected={boolAttr(view.isSelected(hunkIndex, lineIndex))} |
| 197 | + > |
| 198 | + {getDisplayLineNo(line, line.newLineNo)} |
| 199 | + </div> |
196 | 200 | </div> |
197 | | - <div class="w-full pl-[1rem] {lineType.classes}"> |
| 201 | + <div |
| 202 | + class="selected-indicator w-full pl-[1rem] {lineType.classes}" |
| 203 | + data-selection-start={boolAttr(view.isSelectionStart(hunkIndex, lineIndex))} |
| 204 | + data-selection-end={boolAttr(view.isSelectionEnd(hunkIndex, lineIndex))} |
| 205 | + {@attach (element) => { |
| 206 | + onMount(() => { |
| 207 | + if (jumpToSelection && selection && selection.hunk === hunkIndex && selectionMidpoint === lineIndex) { |
| 208 | + jumpToSelection = false; |
| 209 | + // Need to schedule because otherwise the vlist rendering surrounding elements may shift things |
| 210 | + // and cause the element to scroll to the wrong position |
| 211 | + // This is not 100% reliable but is good enough for now |
| 212 | + const scheduledJump = setTimeout(() => { |
| 213 | + element.scrollIntoView({ block: "center", inline: "center" }); |
| 214 | + }, 200); |
| 215 | + return () => { |
| 216 | + if (scheduledJump) { |
| 217 | + clearTimeout(scheduledJump); |
| 218 | + } |
| 219 | + }; |
| 220 | + } |
| 221 | + }); |
| 222 | + }} |
| 223 | + > |
198 | 224 | {@render lineContentWrapper(line, hunkIndex, lineIndex, lineType, innerPatchLineTypeProps[line.innerPatchLineType])} |
199 | 225 | </div> |
200 | 226 | {/snippet} |
|
209 | 235 | > |
210 | 236 | {#each diffViewerPatch.hunks as hunk, hunkIndex (hunkIndex)} |
211 | 237 | {#each hunk.lines as line, lineIndex (lineIndex)} |
212 | | - {@render renderLine(line, hunkIndex, lineIndex)} |
| 238 | + {@render renderLine(line, hunk, hunkIndex, lineIndex)} |
213 | 239 | {/each} |
214 | 240 | {/each} |
215 | 241 | </div> |
|
266 | 292 | left: -0.75rem; |
267 | 293 | top: 0; |
268 | 294 | } |
| 295 | +
|
| 296 | + .selected-indicator[data-selected] { |
| 297 | + box-shadow: inset -4px 0 0 0 var(--hunk-header-fg); |
| 298 | + } |
| 299 | + .selected-indicator[data-selection-start] { |
| 300 | + box-shadow: inset 0 1px 0 0 var(--hunk-header-fg); |
| 301 | + } |
| 302 | + .selected-indicator[data-selection-end] { |
| 303 | + box-shadow: inset 0 -1px 0 0 var(--hunk-header-fg); |
| 304 | + } |
| 305 | + .selected-indicator[data-selection-start][data-selection-end] { |
| 306 | + box-shadow: |
| 307 | + inset 0 1px 0 0 var(--hunk-header-fg), |
| 308 | + inset 0 -1px 0 0 var(--hunk-header-fg); |
| 309 | + } |
269 | 310 | </style> |
0 commit comments