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
+96
View File
@@ -0,0 +1,96 @@
import { describe, it, expect } from 'vitest';
import { depIdAt, idLine } from '../src/autocomplete.js';
import { parse } from '../src/parser.js';
/* Sprung entlang einer Abhängigkeit (D67, SPEC §9): Strg+Klick auf eine ID in
einem `:#…`-Token springt zur Zeile, die die ID vergibt. Hier die beiden
entscheidbaren Hälften: welche ID unter der Schreibmarke steht (depIdAt)
und welche Zeile eine ID vergibt (idLine, erste Vergabe gewinnt). */
/* Schreibmarken-Position aus einer Markierung: `at('- X :#a|uth', '|')`
liefert Text ohne die Marke und die Position der Marke. */
const at = marked => {
const caret = marked.indexOf('|');
return [marked.slice(0, caret) + marked.slice(caret + 1), caret];
};
describe('depIdAt — die ID unter der Schreibmarke (D67)', () => {
it('Schreibmarke in einem einfachen Token liefert die ID', () => {
expect(depIdAt(...at('- [ ] Frontend :#a|pi'))).toBe('api');
});
it('jede ID der Liste ist ansteuerbar', () => {
expect(depIdAt(...at('- X :#a|uth,#db'))).toBe('auth');
expect(depIdAt(...at('- X :#auth,#d|b'))).toBe('db');
});
it('Schreibmarke am Token-Anfang und -Ende zählt mit', () => {
expect(depIdAt(...at('- X |:#auth'))).toBe('auth');
expect(depIdAt(...at('- X :#auth|'))).toBe('auth');
});
it('Kopf-Form: das Token direkt hinter der Knoten-ID', () => {
expect(depIdAt(...at('- #auth:#d|b: Backend'))).toBe('db');
});
it('eingeklammerte Erwähnung bleibt Zitat (§1/D37)', () => {
expect(depIdAt(...at('- X (:#a|uth,#api)'))).toBe(null);
});
it('ein Doppelpunkt im Label ist kein Token-Anfang', () => {
expect(depIdAt(...at('- Regel: #x| gilt'))).toBe(null);
expect(depIdAt(...at('- time:#n|ote'))).toBe(null);
});
it('im Kommentar wird nicht gesprungen', () => {
expect(depIdAt(...at('- X %% siehe :#a|uth'))).toBe(null);
});
it('im Beschreibungsteil hinter --- wird nicht gesprungen', () => {
expect(depIdAt(...at('- X\n---\n#x\n siehe :#a|uth'))).toBe(null);
});
it('ein `:#` in einer URL ist keins', () => {
expect(depIdAt(...at('- X https://ex.org/:#a|nchor'))).toBe(null);
});
it('die Schreibmarke neben dem Token trifft nichts', () => {
expect(depIdAt(...at('- Front|end :#api'))).toBe(null);
});
it('in einer Fortsetzungszeile (D59) wird das Token erkannt', () => {
expect(depIdAt(...at('- Langer Knoten \\\n :#a|pi'))).toBe('api');
});
it('mehrere Token je Zeile: das getroffene zählt', () => {
expect(depIdAt(...at('- X :#a :#b|'))).toBe('b');
});
});
describe('idLine — die Zeile, die eine ID vergibt', () => {
const roots = txt => parse(txt).roots;
it('findet die Zeile der Definition, auch tief im Baum', () => {
const r = roots(`- Wurzel
- #auth: Backend
- #auth.db: Datenbank`);
expect(idLine(r, 'auth')).toBe(2);
expect(idLine(r, 'auth.db')).toBe(3);
});
it('bei doppelter ID gewinnt die erste Vergabe (D36/D39)', () => {
const r = roots(`- #x: Erste
- #x: Zweite`);
expect(idLine(r, 'x')).toBe(1);
});
it('unbekannte ID liefert null', () => {
expect(idLine(roots('- #a: X'), 'fehlt')).toBe(null);
});
it('Vorwärts-Referenz: das Ziel darf weiter unten stehen', () => {
const r = roots(`- Nutzer :#ziel
- #ziel: Das Ziel`);
expect(idLine(r, 'ziel')).toBe(2);
});
});