|
2 | 2 | import { onMount, onDestroy } from 'svelte' |
3 | 3 | import type { CssLocation } from '$lib/css-location' |
4 | 4 | import { highlight_css } from './use-css-highlight' |
| 5 | + import Icon from '$components/Icon.svelte' |
5 | 6 |
|
6 | 7 | type BaseProps = { |
7 | 8 | css?: string |
|
125 | 126 |
|
126 | 127 | let chunks = [ |
127 | 128 | { |
128 | | - start: 0, |
129 | | - covered: line_coverage[0] === 1, |
130 | | - end: 0, |
131 | | - size: 0, |
| 129 | + start_line: 0, |
| 130 | + is_covered: line_coverage[0] === 1, |
| 131 | + end_line: 0, |
| 132 | + size: 0 |
132 | 133 | } |
133 | 134 | ] |
134 | 135 |
|
135 | 136 | for (let index = 0; index < line_coverage.length; index++) { |
136 | 137 | let is_covered = line_coverage[index] |
137 | 138 | if (index > 0 && is_covered !== line_coverage[index - 1]) { |
138 | 139 | let last_chunk = chunks.at(-1)! |
139 | | - last_chunk.end = index |
140 | | - last_chunk.size = index - last_chunk.start |
| 140 | + last_chunk.end_line = index |
| 141 | + last_chunk.size = index - last_chunk.start_line |
141 | 142 |
|
142 | 143 | chunks.push({ |
143 | | - start: index, |
144 | | - covered: is_covered === 1, |
145 | | - end: index, |
| 144 | + start_line: index, |
| 145 | + is_covered: is_covered === 1, |
| 146 | + end_line: index, |
146 | 147 | size: 0 |
147 | 148 | }) |
148 | 149 | } |
149 | 150 | } |
150 | 151 |
|
151 | 152 | let last_chunk = chunks.at(-1)! |
152 | | - last_chunk.size = line_coverage.length - last_chunk.start |
| 153 | + last_chunk.size = line_coverage.length - last_chunk.start_line |
153 | 154 |
|
154 | 155 | return chunks |
155 | 156 | }) |
| 157 | +
|
| 158 | + function scroll_to_line(line: number) { |
| 159 | + body?.scrollTo({ |
| 160 | + top: line * LINE_HEIGHT, |
| 161 | + behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth' |
| 162 | + }) |
| 163 | + } |
| 164 | +
|
| 165 | + function jump_to_next_uncovered() { |
| 166 | + if (!line_number_chunks) return |
| 167 | +
|
| 168 | + let current_scroll_offset = body?.scrollTop || 0 |
| 169 | +
|
| 170 | + let next_uncovered_chunk = line_number_chunks.findIndex((chunk) => { |
| 171 | + if (chunk.is_covered) return false |
| 172 | + let chunk_top = chunk.start_line * LINE_HEIGHT |
| 173 | + return chunk_top > current_scroll_offset |
| 174 | + }) |
| 175 | +
|
| 176 | + let next_chunk = line_number_chunks[next_uncovered_chunk] || line_number_chunks.find((chunk) => !chunk.is_covered) |
| 177 | + scroll_to_line(next_chunk.start_line) |
| 178 | + } |
| 179 | +
|
| 180 | + function jump_to_previous_uncovered() { |
| 181 | + if (!line_number_chunks) return |
| 182 | +
|
| 183 | + let current_scroll_offset = body?.scrollTop || 0 |
| 184 | +
|
| 185 | + let previous_uncovered_chunk = line_number_chunks.findLastIndex((chunk) => { |
| 186 | + if (chunk.is_covered) return false |
| 187 | + let chunk_top = chunk.start_line * LINE_HEIGHT |
| 188 | + return chunk_top < current_scroll_offset |
| 189 | + }) |
| 190 | +
|
| 191 | + let next_chunk = |
| 192 | + line_number_chunks[previous_uncovered_chunk] || line_number_chunks.findLast((chunk) => !chunk.is_covered) |
| 193 | + scroll_to_line(next_chunk.start_line) |
| 194 | + } |
156 | 195 | </script> |
157 | 196 |
|
158 | 197 | <!-- TODO: get rid of #key (only needed because of buggy use:highlight_css) |
|
167 | 206 | style:--pre-line-number-width={line_number_width} |
168 | 207 | style:height="calc({total_lines + 1} * var(--pre-line-height))" |
169 | 208 | > |
| 209 | + {#if show_coverage && line_number_chunks && line_number_chunks.length > 1} |
| 210 | + {@const uncovered_blocks_count = line_number_chunks.filter((c) => !c.is_covered).length} |
| 211 | + <div class="toolbar"> |
| 212 | + <p> |
| 213 | + {uncovered_blocks_count} un-covered {uncovered_blocks_count === 1 ? 'block' : 'blocks'} |
| 214 | + </p> |
| 215 | + <button type="button" onclick={jump_to_previous_uncovered} title="Go to the previous un-covered block"> |
| 216 | + <span class="sr-only">Go to the previous un-covered block</span> |
| 217 | + <Icon name="chevron-up" size={12} /> |
| 218 | + </button> |
| 219 | + <button type="button" onclick={jump_to_next_uncovered} title="Go to the next un-covered block"> |
| 220 | + <span class="sr-only">Go to the next un-covered block</span> |
| 221 | + <Icon name="chevron-down" size={12} /> |
| 222 | + </button> |
| 223 | + </div> |
| 224 | + {/if} |
170 | 225 | {#if show_line_numbers} |
171 | 226 | <div class="line-numbers" aria-hidden="true"> |
172 | | - {#if show_coverage === true && line_number_chunks !== undefined} |
173 | | - {#each line_number_chunks as chunk (chunk.start)} |
174 | | - <div class={['line-number-range', { uncovered: !chunk.covered }]}> |
175 | | - {Array.from({ length: chunk.size }, (_, i) => i + 1 + chunk.start) |
| 227 | + {#if show_coverage === true && line_number_chunks && line_number_chunks.length > 0} |
| 228 | + {#each line_number_chunks as chunk (chunk.start_line)} |
| 229 | + <div class={['line-number-range', { uncovered: !chunk.is_covered }]}> |
| 230 | + {Array.from({ length: chunk.size }, (_, i) => i + 1 + chunk.start_line) |
176 | 231 | .join('\n') |
177 | 232 | .trim()} |
178 | 233 | </div> |
|
225 | 280 | } |
226 | 281 | } |
227 | 282 |
|
228 | | - & > * { |
| 283 | + & .line-numbers, |
| 284 | + & pre { |
229 | 285 | padding-block: var(--space-2); |
230 | 286 | line-height: var(--pre-line-height); |
231 | 287 | font-family: var(--font-mono); |
232 | 288 | font-size: var(--size-specimen); |
233 | 289 | } |
234 | 290 | } |
235 | 291 |
|
| 292 | + .toolbar { |
| 293 | + position: sticky; |
| 294 | + top: 0; |
| 295 | + right: 0; |
| 296 | + left: 0; |
| 297 | + grid-row: 1 / -1; |
| 298 | + grid-column: 1 / -1; |
| 299 | + z-index: 1; |
| 300 | + background-color: var(--bg-200); |
| 301 | + display: flex; |
| 302 | + align-items: center; |
| 303 | + justify-content: space-between; |
| 304 | + gap: var(--space-2); |
| 305 | + padding-inline: var(--space-2); |
| 306 | + padding-block: var(--space-2); |
| 307 | +
|
| 308 | + p { |
| 309 | + margin-inline-end: auto; |
| 310 | + font-size: var(--size-sm); |
| 311 | + } |
| 312 | +
|
| 313 | + button { |
| 314 | + padding-inline: var(--space-2); |
| 315 | + padding-block: var(--space-1); |
| 316 | + background-color: transparent; |
| 317 | +
|
| 318 | + &:hover, |
| 319 | + &:focus { |
| 320 | + background-color: var(--bg-400); |
| 321 | + } |
| 322 | + } |
| 323 | + } |
| 324 | +
|
236 | 325 | .line-numbers { |
237 | 326 | color: var(--fg-400); |
238 | 327 | text-align: end; |
|
0 commit comments