feat: Strg+Klick auf :#id springt zur Zeile der referenzierten ID (D67)

Strg/Cmd+Klick auf eine Abhängigkeits-ID im Textfeld — auch jede ID der
Liste und die Kopf-Form #auth:#db — markiert die Zeile, die die ID vergibt;
Tastatur-Pendant Strg+Enter. Erste Vergabe gewinnt (D36/D39), unbekannte IDs
tun still nichts. Headless als depIdAt/idLine in autocomplete.js, 16 Tests;
Legenden-Zeile hint_jump in allen neun Sprachen erweitert.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-25 08:59:58 +02:00
co-authored by Claude Fable 5
parent 54e27091ce
commit 549c12191e
8 changed files with 288 additions and 17 deletions
+66 -6
View File
@@ -1,10 +1,12 @@
/* ID-Vorschläge beim Tippen von Abhängigkeiten (D63).
/* ID-Vorschläge beim Tippen von Abhängigkeiten (D63) und der Sprung entlang
einer Abhängigkeit (Strg+Klick auf `:#ziel`, D67).
Eine Eingabehilfe wie die ID-Kurzform (D55), keine Notation: Der Parser
sieht nie etwas davon, SPEC und llms.md bleiben unberührt. Hier steht, WAS
gilt — wann ein `:#…`-Kontext vorliegt und welche IDs dazu passen; app.js
verdrahtet nur (Popup, Tasten, Einfügen). Frontend-Hausregel: Was
entscheidbar ist, gehört in ein Modul (D54-Nachtrag 3). */
Eingabehilfen wie die ID-Kurzform (D55), keine Notation: Der Parser sieht
nie etwas davon, SPEC und llms.md bleiben unberührt. Hier steht, WAS
gilt — wann ein `:#…`-Kontext vorliegt, welche IDs dazu passen und welche
ID unter der Schreibmarke steht; app.js verdrahtet nur (Popup, Tasten,
Einfügen, Sprung). Frontend-Hausregel: Was entscheidbar ist, gehört in ein
Modul (D54-Nachtrag 3). */
const ID_CHARS = '[\\p{L}\\p{N}._-]';
@@ -69,6 +71,64 @@ export function collectIds(roots){
return out;
}
/* Welche Abhängigkeits-ID steht an dieser Stelle des Textes? (D67, für den
Sprung per Strg+Klick.) null, oder die ID (ohne `#`), auf der die
Schreibmarke in einem `:#a,#b`-Token steht. Erkannt wird dieselbe Form wie
oben (alleinstehend angesetzt oder Kopf-Form `#auth:#db`) — nur vollständig
statt bis zur Schreibmarke, denn geklickt wird auf fertige Token. Kein
Treffer im Kommentar, im Beschreibungsteil hinter `---` und in einer URL
(die Extraktion §1 nimmt die URL zuerst — ein `:#` darin ist keins). */
const URL_RE = /https?:\/\/\S+/i;
const DEP_TOKEN_G = new RegExp(':#' + ID_CHARS + '+(?:,#' + ID_CHARS + '+)*', 'gu');
export function depIdAt(text, caret){
const sol = text.lastIndexOf('\n', caret - 1) + 1;
let eol = text.indexOf('\n', caret);
if(eol === -1) eol = text.length;
const line = text.slice(sol, eol);
const col = caret - sol;
for(const l of text.slice(0, sol).split('\n')){
if(/^\s*-{3,}\s*$/.test(l)) return null;
}
const k = line.indexOf('%%');
if(k !== -1 && col >= k) return null;
const head = k === -1 ? line : line.slice(0, k);
const u = URL_RE.exec(head);
if(u && col > u.index && col < u.index + u[0].length) return null;
/* Kopf-Form: unmittelbar hinter der Knoten-ID der Zeile (dem ersten
alleinstehenden `#`-Token, Extraktion Schritt 6) darf das Token ansetzen. */
const own = OWN_ID_RE.exec(head);
const idEnd = own ? own.index + own[0].length : -1;
DEP_TOKEN_G.lastIndex = 0;
let m;
while((m = DEP_TOKEN_G.exec(head))){
const p = m.index;
/* Alleinstehend angesetzt — `(:#a,#b)` bleibt damit Zitat (§1/D37). */
if(p !== idEnd && p > 0 && !/[ \t]/.test(head[p - 1])) continue;
const end = p + m[0].length;
if(col < p || col > end) continue;
let s = p + 1; /* hinter dem `:` */
for(const part of m[0].slice(1).split(',')){
const e = s + part.length;
if(col <= e) return part.slice(1); /* ohne `#` */
s = e + 1; /* hinter dem `,` */
}
return null;
}
return null;
}
/* Die Zeile, die eine ID vergibt — bei doppelter ID die ERSTE Vergabe, wie
überall bei der Auflösung (D36/D39). null für unbekannte IDs. */
export function idLine(roots, id){
for(const n of roots){
if(n.id === id) return n.line;
const hit = idLine(n.children, id);
if(hit) return hit;
}
return null;
}
/* Passende Kandidaten: erst Präfix-Treffer, dann Teilstring-Treffer, je in
Dokumentreihenfolge; Groß-/Kleinschreibung egal (die IDs selbst bleiben,
wie sie geschrieben sind). Leeres Fragment (direkt nach `:#`) zeigt alle. */