~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/knotless

git that lives in your Atmosphere account

knotless.dev
1 collaborator

knotless 0.13.0 Latest 2 packages, 1 image

1
10

A line's number marks the whole line in place, and shift marks a range

Merged chadtmiller.com asked to merge into main from chadtmiller.com/knotless:web/line-marks
web/src/lib/organisms/CodeView.svelte+26−10
@@ -1,16 +1,32 @@
<script lang="ts">
import { page } from '$app/state';
import { highlight, target } from '#lib/highlight.ts';
import type { Snippet } from 'svelte';
/** A text file, with a gutter of line numbers to link to, and before it `gutter`, a column as tall as the text, when given. */
import { goto } from '$app/navigation';
import { highlight } from '#lib/highlight.ts';
import { onMount, type Snippet } from 'svelte';
/** A text file, with a gutter of line numbers to link to, and before it `gutter`, a column as tall as the text, when given. A line's number marks it, shift and a second marks the lines between. */
let { text, lines, ranges, gutter }: { text: string; lines: number; ranges?: string | null; gutter?: Snippet } = $props();
let code: HTMLElement | undefined = $state();
$effect(() => {
void text;
const m = /^#L(\d+)$/.exec(page.url.hash);
target(code, m ? +m[1] : null);
return () => target(undefined, null);
/** The lines `#L5` or `#L5-L9` marks, first to last, or null. */
const marked = $derived.by(() => {
const m = /^#L(\d+)(?:-L(\d+))?$/.exec(page.url.hash);
if (!m) return null;
const a = +m[1];
const b = m[2] ? +m[2] : a;
const first = Math.max(1, Math.min(a, b));
const last = Math.min(lines, Math.max(a, b));
return first <= last ? { first, last } : null;
});
let ln: HTMLElement | undefined = $state();
onMount(() => {
if (marked) ln?.querySelector(`#L${marked.first}`)?.scrollIntoView({ block: 'center' });
});
function mark(e: MouseEvent) {
const a = (e.target as Element).closest('a');
const n = a ? +a.id.slice(1) : 0;
if (!n || e.metaKey || e.ctrlKey || e.altKey) return;
e.preventDefault();
const hash = e.shiftKey && marked ? (n === marked.first ? `#L${n}` : `#L${marked.first}-L${n}`) : `#L${n}`;
goto(hash, { replace: true, reset: false });
}
</script>
<div class="code" class:gutted={gutter}>{#if gutter}{@render gutter()}{/if}<pre class="ln" aria-hidden="true">{#each { length: lines }, i (i)}<a id="L{i + 1}" href="#L{i + 1}">{i + 1}</a>{'\n'}{/each}</pre><pre class="src" use:highlight={{ ranges, text }}><code bind:this={code}>{text}</code></pre></div>
<div class="code" class:gutted={gutter}>{#if marked}<div class="marked" style:--first={marked.first} style:--count={marked.last - marked.first + 1}></div>{/if}{#if gutter}{@render gutter()}{/if}<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_noninteractive_element_interactions --><pre class="ln" aria-hidden="true" bind:this={ln} onclick={mark}>{#each { length: lines }, i (i)}<a id="L{i + 1}" href="#L{i + 1}">{i + 1}</a>{'\n'}{/each}</pre><pre class="src" use:highlight={{ ranges, text }}><code>{text}</code></pre></div>
web/src/lib/highlight.ts+0−17
@@ -152,20 +152,3 @@ export function fences(el: Element, _html: string) {
destroy: clear
};
}
/** Marks line `line` of the text in `el`, or nothing. */
export function target(el: Element | undefined, line: number | null) {
if (!globalThis.CSS?.highlights) return;
CSS.highlights.delete('target');
const text = el && line ? textOf(el) : null;
if (!text || !line) return;
const data = text.data;
let a = 0;
for (let i = 1; i < line; i++) {
a = data.indexOf('\n', a) + 1;
if (!a) return;
}
const e = data.indexOf('\n', a);
const r = new StaticRange({ startContainer: text, startOffset: a, endContainer: text, endOffset: e < 0 ? data.length : e + 1 });
CSS.highlights.set('target', new Highlight(r));
}
web/src/app.css+5−3
@@ -562,11 +562,13 @@ a.row:hover{background:var(--hover)}
.release-files li{font-size:var(--text-sm)}
.release-files .file-name{flex:1;min-width:0;overflow-wrap:anywhere}
.release-files .digest{font:var(--text-xs) var(--font-mono);color:var(--faint)}
.code{display:grid;grid-template-columns:auto minmax(0,1fr)}
.code{position:relative;display:grid;grid-template-columns:auto minmax(0,1fr)}
/* The lines a link marks: a band across the whole row, behind the numbers and the text. */
.code .marked{position:absolute;left:0;right:0;top:calc(var(--space-3) + (var(--first) - 1) * var(--leading-code));height:calc(var(--count) * var(--leading-code));background:var(--target);pointer-events:none}
.code>:not(.marked){position:relative}
.code pre{margin:0;padding:var(--space-3) 0;line-height:var(--leading-code);tab-size:4}
.code .ln{text-align:right;color:var(--faint);user-select:none;padding:var(--space-3) var(--space-3) var(--space-3) var(--pad-x);min-width:3.5rem}
.code .ln a{color:inherit}
.code .ln a:target{color:var(--fg)}
.code .src{overflow-x:auto;padding:var(--space-3) var(--pad-x)}
/* Blame: a column beside the code, each run of lines as tall as they are, with its commit along its first line. */
.code.gutted{grid-template-columns:var(--blame-width) auto minmax(0,1fr)}
@@ -1620,7 +1622,7 @@ textarea.field{resize:vertical;font-family:var(--font-mono);font-size:var(--text
/* ── Highlighted code ─────────────────────────────────────────────── */
::highlight(comment){color:var(--comment)}::highlight(string){color:var(--string)}::highlight(keyword){color:var(--keyword)}::highlight(number){color:var(--number)}::highlight(function){color:var(--function)}::highlight(type){color:var(--type)}::highlight(tag){color:var(--tag)}::highlight(attr){color:var(--attr)}
::highlight(xins){background-color:var(--ins-x);color:var(--fg)}::highlight(xdel){background-color:var(--del-x);color:var(--fg)}::highlight(target){background-color:var(--target)}
::highlight(xins){background-color:var(--ins-x);color:var(--fg)}::highlight(xdel){background-color:var(--del-x);color:var(--fg)}
/* ── Admin ────────────────────────────────────────────────────────── */