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
+4
View File
@@ -17,6 +17,10 @@ the git history of `docs/examples/werkbaum.werkbaum`. A day can therefore carry
a link without having a note (someone forgot to write one) — but never the a link without having a note (someone forgot to write one) — but never the
reverse. reverse.
## 2026-08-25
- Ctrl+click a dependency `:#id` in the text jumps to the line that defines the ID — Ctrl+Enter does the same at the caret
## 2026-08-24 ## 2026-08-24
- A missing size is now estimated from the sub-packages — at least the largest child size, one step more from three children of that size - A missing size is now estimated from the sub-packages — at least the largest child size, one step more from three children of that size
+66
View File
@@ -4990,3 +4990,69 @@ stehen. 424 Tests, davon 19 neue in `tests/assumed.test.js`. Gegenprobe per
Mutation: Stufe-ab-drei entfernt → genau die drei danach benannten Mutation: Stufe-ab-drei entfernt → genau die drei danach benannten
Zusicherungen fallen; disjunktiv Maximum statt Minimum → genau eine; alte Zusicherungen fallen; disjunktiv Maximum statt Minimum → genau eine; alte
M-Pauschale in `ownCost` zurückgebaut → genau die zwei Kosten-Tests. M-Pauschale in `ownCost` zurückgebaut → genau die zwei Kosten-Tests.
## D67 — Strg+Klick folgt einer Abhängigkeit zur Zeile ihrer ID
Gewünscht vom Nutzer: Strg+Klick auf eine ID-Referenz wie `:#ziel` im Textfeld
soll zur referenzierten ID springen; Alt+Klick bleibt wie gehabt und
fokussiert den Knoten im Diagramm. Die Lücke ist real: Abhängigkeiten zeigen
bewusst quer durch den Baum (D34), und im mitgelieferten Plan liegt das Ziel
oft hunderte Zeilen entfernt. Die ID-Vorschläge (D63) helfen beim
**Schreiben** einer Abhängigkeit — beim **Lesen** blieb nur die Textsuche.
**Die Schreibmarke ist der Treffer, kein eigenes Hit-Testing.** Ein Klick ins
Textfeld setzt die Schreibmarke, bevor das `click`-Ereignis läuft —
`depIdAt(text, caret)` liest also einfach an `selectionStart`, welche ID dort
steht. Kein Pixel-Rechnen, kein Spiegel-`div`; dieselbe Sparsamkeit wie beim
Alt+Klick im Textfeld (D25-Nachtrag), der auch nur die Cursor-Zeile nimmt.
**Erkannt wird dieselbe Form wie bei den ID-Vorschlägen (D63):** das Token
alleinstehend angesetzt oder in der Kopf-Form `#auth:#db`; kein Treffer im
Kommentar, im Beschreibungsteil hinter `---` und innerhalb einer URL, und
`(:#a,#b)` bleibt Zitat (§1/D37). Jede ID der Liste ist einzeln ansteuerbar —
die Schreibmarke wählt das Segment. **Benannte Grenze:** Der Parser erkennt
nach der Extraktion auch Randformen wie `(M):#b` als Abhängigkeit (die
Größen-Entfernung macht das Token alleinstehend); die Klick-Erkennung auf der
rohen Zeile tut das nicht. Dort geschieht schlicht nichts — der harmlose
Fehlermodus, und dieselbe Vereinfachung, die D63 bereits gewählt hat.
**Aufgelöst wird zur ersten Vergabe** (`idLine`, Dokumentreihenfolge) — die
Regel aus D36/D39, nach der überall aufgelöst wird. Eine unbekannte ID tut
still nichts: `unknownDep` warnt bereits, ein zweiter Kanal wäre Lärm.
Vorwärts-Referenzen springen nach unten, Zyklen sind schlicht zwei Sprünge.
**Das Ziel bekommt denselben Sprung wie aus dem Diagramm:** `jumpToLine()`
ganze Zeile markiert, in Sicht gescrollt, waagerecht auf Anfang (D49), und
über `caretLine` hebt sich der Zielknoten im Diagramm mit hervor. Kein neues
Idiom für dieselbe Aussage „hier ist es".
**Strg, mit Cmd als macOS-Zwilling.** Alt ist vergeben (Text → Diagramm,
D25); Strg+Klick ist in Editoren und IDEs die etablierte
„zur Definition"-Geste. Auf macOS ist Strg+Klick das Kontextmenü — dort
übernimmt Cmd+Klick (`metaKey`). Tastatur-Pendant ist **Strg+Enter** an der
Schreibmarke im Token — dasselbe Muster wie Alt+Enter (D25); `preventDefault`
nur bei erfolgtem Sprung, sonst bleibt der Browser-Default unberührt. Auf
Touch gibt es kein Strg und **kein Pendant** — bewusst nicht gebaut; der
lange Druck ist vergeben (D25), und ein Knopf im Knoten-Fenster wäre eine
eigene Entscheidung.
**Headless nach Hausregel** (D54-Nachtrag 3): `depIdAt` und `idLine` liegen
in `autocomplete.js` neben den D63-Regeln, mit denen sie sich Zeichenmenge
und Kontext-Ausschlüsse teilen; app.js verdrahtet nur zwei Handler.
Auffindbarkeit über die vorhandene Legenden-Zeile `hint_jump` (erweitert in
allen neun Sprachen, statt eines neuen Schlüssels — das D25-Idiom) und
SPEC §9.
**Nachgemessen** im Browser mit einem **echten** Strg+Klick auf das `#ui` in
`:#api,#ui`: Zeile `- #ui: Oberflaeche (S)` vollständig markiert, Fokus im
Textfeld, `scrollLeft` 0, Diagramm hebt „Oberflaeche" hervor. Strg+Enter im
Token springt ebenso (preventDefault gesetzt, kein Umbruch eingefügt); im
Kommentar, im Label und neben dem Token geschieht nichts. 440 Tests, davon 16
neue in `tests/deplink.test.js`; Gegenprobe per Mutation: Alleinstehend-
Prüfung entfernt → genau die zwei danach benannten Zusicherungen fallen.
**Werkzeuggrenze, wieder dieselbe Sorte wie D53:** Der synthetische
Strg+Enter der Browser-Automatisierung kommt mit **`e.key === ""`** an und
kann den Handler prinzipiell nicht treffen — der erste Prüflauf sah deshalb
wie ein Fehler aus, der keiner war. Geprüft wird das Tastatur-Pendant mit
einem korrekt gebauten `KeyboardEvent`; der Klick-Weg ließ sich dagegen echt
auslösen.
+10
View File
@@ -845,6 +845,16 @@ verknüpft (siehe D25):
nötig und nur beim Zeilenwechsel (sonst ruckelte das Diagramm beim Tippen). nötig und nur beim Zeilenwechsel (sonst ruckelte das Diagramm beim Tippen).
Wer einen Knoten wirklich **sehen** will, sagt es mit Alt. Auf einer Zeile ohne Wer einen Knoten wirklich **sehen** will, sagt es mit Alt. Auf einer Zeile ohne
Knoten geschieht nichts. Knoten geschieht nichts.
- **Text → Text, entlang einer Abhängigkeit:** **Strg+Klick** (macOS auch
Cmd+Klick) auf eine ID in einem Abhängigkeits-Token — `:#ziel`, jede ID der
Liste `:#a,#b`, auch die Kopf-Form `#auth:#ziel` — markiert die **Zeile, die
diese ID vergibt**: derselbe Sprung wie aus dem Diagramm (ganze Zeile
ausgewählt, in Sicht gescrollt), nur innerhalb des Textes. Tastatur:
**Strg+Enter**, wenn die Schreibmarke im Token steht. Bei doppelter ID
gewinnt die erste Vergabe (§1/§4); eine unbekannte ID tut still nichts (sie
ist schon als `unknownDep` gewarnt). Kein Sprung im Kommentar, im
Beschreibungsteil hinter `---` und innerhalb einer URL; Alt+Klick behält
daneben unverändert seine Richtung ins Diagramm. Siehe D67.
- Der **einfache** Klick bleibt unverändert der Link (§6): Ein Knoten mit URL - Der **einfache** Klick bleibt unverändert der Link (§6): Ein Knoten mit URL
öffnet sie weiterhin im neuen Tab. Deshalb Alt und nicht der einfache Klick. öffnet sie weiterhin im neuen Tab. Deshalb Alt und nicht der einfache Klick.
- **Auffindbarkeit:** Solange **Alt gedrückt** ist, zeigen alle Knoten den - **Auffindbarkeit:** Solange **Alt gedrückt** ist, zeigen alle Knoten den
+6
View File
@@ -72,6 +72,7 @@
- [^] #ed.docs.url: Load a document from ?sourceUrl= (S) - [^] #ed.docs.url: Load a document from ?sourceUrl= (S)
- [^] #ed.docs.restore: Restore a shipped document from the menu (XS) - [^] #ed.docs.restore: Restore a shipped document from the menu (XS)
- [^] #ed.jump: Jump between diagram and text (S) - [^] #ed.jump: Jump between diagram and text (S)
- [x] #ed.jump.dep: Ctrl+click follows a dependency to its id (XS)
- [^] #ed.lineno: Line numbers in the text editor (XS) %% the warnings name them - [^] #ed.lineno: Line numbers in the text editor (XS) %% the warnings name them
- [^] #ed.shortid: Type #.kc under #prod-stage, get #prod-stage.kc (XS) %% an input aid, not notation - [^] #ed.shortid: Type #.kc under #prod-stage, get #prod-stage.kc (XS) %% an input aid, not notation
- [^] #ed.depcomplete: Typing :# suggests the known ids (S) %% arrows choose, Enter inserts - [^] #ed.depcomplete: Typing :# suggests the known ids (S) %% arrows choose, Enter inserts
@@ -523,6 +524,11 @@
centres the node. One gesture, both directions — and descriptions count as centres the node. One gesture, both directions — and descriptions count as
part of the node they describe. part of the node they describe.
#ed.jump.dep
Ctrl+click (Cmd on macOS) on an id inside a dependency token like (:#target)
selects the line that defines the id; Ctrl+Enter at the caret does the same.
The first assignment wins for duplicate ids, an unknown id does nothing.
#ed.lineno #ed.lineno
A narrow strip beside the text field showing the logical line numbers, the A narrow strip beside the text field showing the logical line numbers, the
same ones the warnings name. Measured rather than calculated: a wrapped line same ones the warnings name. Measured rather than calculated: a wrapped line
+7 -1
View File
@@ -610,7 +610,13 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
zurückzunehmen. zurückzunehmen.
- **ID-Vorschläge `:#` (D63):** Die Regeln stehen headless in `autocomplete.js` - **ID-Vorschläge `:#` (D63):** Die Regeln stehen headless in `autocomplete.js`
(`depFragment`/`collectIds`/`matchIds`, Tests); app.js verdrahtet nur Popup, (`depFragment`/`collectIds`/`matchIds`, Tests); app.js verdrahtet nur Popup,
Tasten, Einfügen (`writeAt`, undo-fähig). Der Tasten-Handler hängt an Tasten, Einfügen (`writeAt`, undo-fähig). Daneben liegen `depIdAt`/`idLine`
(D67): Strg/Cmd+Klick bzw. Strg+Enter im Textfeld springt von einer
Abhängigkeits-ID zur Zeile ihrer ersten Vergabe — die Schreibmarke ist der
Treffer (`selectionStart`), kein eigenes Hit-Testing. Ein synthetischer
Strg+Enter der Automatisierung kommt mit `e.key === ""` an (Werkzeuggrenze
wie beim synthetischen Strg+Z) — mit korrekt gebautem `KeyboardEvent`
prüfen. Der Tasten-Handler hängt an
`document` in der **Capture-Phase** — die Textfeld-Handler (Tab rückt ein, `document` in der **Capture-Phase** — die Textfeld-Handler (Tab rückt ein,
Esc löst die Tab-Falle, D53) sind früher registriert und kämen sonst zuerst; Esc löst die Tab-Falle, D53) sind früher registriert und kämen sonst zuerst;
`stopPropagation` hält sie nur bei **offener** Liste heraus. `acSuppress` `stopPropagation` hält sie nur bei **offener** Liste heraus. `acSuppress`
+33 -10
View File
@@ -4,7 +4,7 @@ import { computeCheapPlan, freshProdSet, initialCollapsed, nodeKeys, effectiveSt
import { esc, renderTreeHtml, TIP_RULE } from './render.js'; import { esc, renderTreeHtml, TIP_RULE } from './render.js';
import { formatWarning, warningText } from './warnings.js'; import { formatWarning, warningText } from './warnings.js';
import { padUrls } from './remote.js'; import { padUrls } from './remote.js';
import { depFragment, collectIds, matchIds } from './autocomplete.js'; import { depFragment, collectIds, matchIds, depIdAt, idLine } from './autocomplete.js';
import { LS_SNAPS, SNAP_EVERY, parseSnaps, addSnapshot, persistSnaps, snapLabel } import { LS_SNAPS, SNAP_EVERY, parseSnaps, addSnapshot, persistSnaps, snapLabel }
from './snapshots.js'; from './snapshots.js';
/* Neuigkeiten (D58): die git-Historie, zur BAUZEIT eingelesen (Vite-Plugin in /* Neuigkeiten (D58): die git-Historie, zur BAUZEIT eingelesen (Vite-Plugin in
@@ -1630,6 +1630,29 @@ src.addEventListener('keydown', e => {
focusNodeOfCaret(); focusNodeOfCaret();
}); });
/* Strg+Klick (macOS auch Cmd) auf eine Abhängigkeits-ID `:#ziel`, jede ID
der Liste, auch die Kopf-Form `#auth:#ziel` springt zur Zeile, die die ID
vergibt (D67): derselbe Sprung wie aus dem Diagramm, nur innerhalb des
Textes. Alt+Klick behält daneben seine Richtung ins Diagramm. Der Klick hat
die Schreibmarke schon gesetzt, `depIdAt` liest also einfach dort; bei
doppelter ID gewinnt die erste Vergabe (D36/D39), eine unbekannte tut still
nichts (`unknownDep` warnt schon). */
function jumpToDepTarget(){
const id = depIdAt(src.value, src.selectionStart);
if(!id) return false;
const line = idLine(parse(src.value).roots, id);
if(line) jumpToLine(line);
return true;
}
src.addEventListener('click', e => {
if(e.altKey || !(e.ctrlKey || e.metaKey)) return;
jumpToDepTarget();
});
src.addEventListener('keydown', e => {
if(e.key !== 'Enter' || !(e.ctrlKey || e.metaKey) || e.altKey) return;
if(jumpToDepTarget()) e.preventDefault();
});
/* Alt-Modus sichtbar machen: solange Alt gedrückt ist, zeigt jeder Knoten den /* Alt-Modus sichtbar machen: solange Alt gedrückt ist, zeigt jeder Knoten den
Sprung-Cursor und der Knoten unter dem Zeiger einen Petrol-Ring. Das ist die Sprung-Cursor und der Knoten unter dem Zeiger einen Petrol-Ring. Das ist die
einzige Rückmeldung, die auch auf verlinkten Knoten funktioniert (dort gehört einzige Rückmeldung, die auch auf verlinkten Knoten funktioniert (dort gehört
@@ -2220,7 +2243,7 @@ const I18N = {
hint_eff:"Die Knotenfarbe zeigt den effektiven Status (mit Abhängigkeiten); ist der eigene weiter, steht er als Marke unten links.", hint_eff:"Die Knotenfarbe zeigt den effektiven Status (mit Abhängigkeiten); ist der eigene weiter, steht er als Marke unten links.",
hint_desc:"Beschreibungen: \" Zeile unter dem Knoten; Langtext hinter --- als eingerückter #id-Block — beides im Tooltip (”).", hint_desc:"Beschreibungen: \" Zeile unter dem Knoten; Langtext hinter --- als eingerückter #id-Block — beides im Tooltip (”).",
hint_fold:"Falten: - [x] > … startet eingeklappt, < holt hervor; ▾/▸ am Knoten klappt um (Tastatur: ←/→).", hint_fold:"Falten: - [x] > … startet eingeklappt, < holt hervor; ▾/▸ am Knoten klappt um (Tastatur: ←/→).",
hint_jump:"Alt+Klick auf einen Knoten (mobil: langer Druck) springt zur zugehörigen Textzeile; Alt+Klick im Text holt den Knoten ins Bild." hint_jump:"Alt+Klick auf einen Knoten (mobil: langer Druck) springt zur zugehörigen Textzeile; Alt+Klick im Text holt den Knoten ins Bild; Strg+Klick auf eine Abhängigkeit :#id springt zur Zeile dieser ID."
}, },
en: { en: {
subtitle:"Werkbaum Work Breakdown Structure / Lean Pathfinding · Project structure editor (also feature-tree & requirements)", subtitle:"Werkbaum Work Breakdown Structure / Lean Pathfinding · Project structure editor (also feature-tree & requirements)",
@@ -2324,7 +2347,7 @@ const I18N = {
hint_eff:"Node colour shows the effective status (with dependencies); if its own is further along, it appears as a mark at the bottom left.", hint_eff:"Node colour shows the effective status (with dependencies); if its own is further along, it appears as a mark at the bottom left.",
hint_desc:"Descriptions: a \" line below the node; long text behind --- as an indented #id block — both in the tooltip (”).", hint_desc:"Descriptions: a \" line below the node; long text behind --- as an indented #id block — both in the tooltip (”).",
hint_fold:"Folding: - [x] > … starts collapsed, < brings it back; ▾/▸ on a node toggles (keyboard: ←/→).", hint_fold:"Folding: - [x] > … starts collapsed, < brings it back; ▾/▸ on a node toggles (keyboard: ←/→).",
hint_jump:"Alt+click a node (long press on touch) jumps to its line in the text; Alt+click in the text brings the node into view." hint_jump:"Alt+click a node (long press on touch) jumps to its line in the text; Alt+click in the text brings the node into view; Ctrl+click a dependency :#id jumps to that ID's line."
}, },
es: { es: {
subtitle:"Werkbaum EDT / Lean Pathfinding · Editor de estructura de proyectos (también árboles de características y requisitos)", subtitle:"Werkbaum EDT / Lean Pathfinding · Editor de estructura de proyectos (también árboles de características y requisitos)",
@@ -2428,7 +2451,7 @@ const I18N = {
hint_eff:"El color del nodo muestra el estado efectivo (con dependencias); si el propio va más adelante, aparece como marca abajo a la izquierda.", hint_eff:"El color del nodo muestra el estado efectivo (con dependencias); si el propio va más adelante, aparece como marca abajo a la izquierda.",
hint_desc:"Descripciones: línea \" bajo el nodo; texto largo tras --- como bloque #id sangrado — ambos en el tooltip (”).", hint_desc:"Descripciones: línea \" bajo el nodo; texto largo tras --- como bloque #id sangrado — ambos en el tooltip (”).",
hint_fold:"Plegado: - [x] > … empieza plegado, < lo recupera; ▾/▸ en el nodo alterna (teclado: ←/→).", hint_fold:"Plegado: - [x] > … empieza plegado, < lo recupera; ▾/▸ en el nodo alterna (teclado: ←/→).",
hint_jump:"Alt+clic en un nodo (pulsación larga en táctil) salta a su línea en el texto; Alt+clic en el texto trae el nodo a la vista." hint_jump:"Alt+clic en un nodo (pulsación larga en táctil) salta a su línea en el texto; Alt+clic en el texto trae el nodo a la vista; Ctrl+clic en una dependencia :#id salta a la línea de esa ID."
}, },
fr: { fr: {
subtitle:"Werkbaum WBS / Lean Pathfinding · Éditeur de structure de projet (aussi pour arbres de fonctionnalités et d'exigences)", subtitle:"Werkbaum WBS / Lean Pathfinding · Éditeur de structure de projet (aussi pour arbres de fonctionnalités et d'exigences)",
@@ -2532,7 +2555,7 @@ const I18N = {
hint_eff:"La couleur du nœud montre le statut effectif (avec dépendances) ; si le sien est plus avancé, il apparaît en marque en bas à gauche.", hint_eff:"La couleur du nœud montre le statut effectif (avec dépendances) ; si le sien est plus avancé, il apparaît en marque en bas à gauche.",
hint_desc:"Descriptions : ligne \" sous le nœud ; texte long après --- en bloc #id indenté — les deux dans linfobulle (”).", hint_desc:"Descriptions : ligne \" sous le nœud ; texte long après --- en bloc #id indenté — les deux dans linfobulle (”).",
hint_fold:"Pliage : - [x] > … démarre replié, < le fait ressortir ; ▾/▸ sur le nœud bascule (clavier : ←/→).", hint_fold:"Pliage : - [x] > … démarre replié, < le fait ressortir ; ▾/▸ sur le nœud bascule (clavier : ←/→).",
hint_jump:"Alt+clic sur un nœud (appui long sur tactile) saute à sa ligne dans le texte ; Alt+clic dans le texte amène le nœud à l’écran." hint_jump:"Alt+clic sur un nœud (appui long sur tactile) saute à sa ligne dans le texte ; Alt+clic dans le texte amène le nœud à l’écran ; Ctrl+clic sur une dépendance :#id saute à la ligne de cet ID."
}, },
pl: { pl: {
subtitle:"Werkbaum WBS / Lean Pathfinding · Edytor struktury projektów (również dla drzew funkcji i wymagań)", subtitle:"Werkbaum WBS / Lean Pathfinding · Edytor struktury projektów (również dla drzew funkcji i wymagań)",
@@ -2636,7 +2659,7 @@ const I18N = {
hint_eff:"Kolor węzła pokazuje status efektywny (z zależnościami); jeśli własny jest dalej, widnieje jako znacznik u dołu po lewej.", hint_eff:"Kolor węzła pokazuje status efektywny (z zależnościami); jeśli własny jest dalej, widnieje jako znacznik u dołu po lewej.",
hint_desc:"Opisy: wiersz \" pod węzłem; dłuższy tekst za --- jako wcięty blok #id — oba w podpowiedzi (”).", hint_desc:"Opisy: wiersz \" pod węzłem; dłuższy tekst za --- jako wcięty blok #id — oba w podpowiedzi (”).",
hint_fold:"Zwijanie: - [x] > … zaczyna zwinięte, < przywraca; ▾/▸ na węźle przełącza (klawiatura: ←/→).", hint_fold:"Zwijanie: - [x] > … zaczyna zwinięte, < przywraca; ▾/▸ na węźle przełącza (klawiatura: ←/→).",
hint_jump:"Alt+kliknięcie węzła (długie naciśnięcie na dotyku) przechodzi do jego wiersza w tekście; Alt+kliknięcie w tekście pokazuje węzeł na diagramie." hint_jump:"Alt+kliknięcie węzła (długie naciśnięcie na dotyku) przechodzi do jego wiersza w tekście; Alt+kliknięcie w tekście pokazuje węzeł na diagramie; Ctrl+kliknięcie zależności :#id przechodzi do wiersza tego ID."
}, },
ru: { ru: {
subtitle:"Werkbaum СДР / Lean Pathfinding · Редактор структуры проектов (также для деревьев функций и требований)", subtitle:"Werkbaum СДР / Lean Pathfinding · Редактор структуры проектов (также для деревьев функций и требований)",
@@ -2740,7 +2763,7 @@ const I18N = {
hint_eff:"Цвет узла показывает фактический статус (с учётом зависимостей); если собственный дальше, он показан меткой слева внизу.", hint_eff:"Цвет узла показывает фактический статус (с учётом зависимостей); если собственный дальше, он показан меткой слева внизу.",
hint_desc:"Описания: строка \" под узлом; длинный текст после --- как блок #id с отступом — оба в подсказке (”).", hint_desc:"Описания: строка \" под узлом; длинный текст после --- как блок #id с отступом — оба в подсказке (”).",
hint_fold:"Сворачивание: - [x] > … открывается свёрнутым, < возвращает; ▾/▸ на узле переключает (клавиши: ←/→).", hint_fold:"Сворачивание: - [x] > … открывается свёрнутым, < возвращает; ▾/▸ на узле переключает (клавиши: ←/→).",
hint_jump:"Alt+клик по узлу (долгое нажатие на сенсоре) переходит к его строке в тексте; Alt+клик в тексте показывает узел на диаграмме." hint_jump:"Alt+клик по узлу (долгое нажатие на сенсоре) переходит к его строке в тексте; Alt+клик в тексте показывает узел на диаграмме; Ctrl+клик по зависимости :#id переходит к строке этого ID."
}, },
hi: { hi: {
subtitle:"Werkbaum WBS / Lean Pathfinding · परियोजना संरचना संपादक (फ़ीचर और रिक्वायरमेंट ट्री के लिए भी)", subtitle:"Werkbaum WBS / Lean Pathfinding · परियोजना संरचना संपादक (फ़ीचर और रिक्वायरमेंट ट्री के लिए भी)",
@@ -2844,7 +2867,7 @@ const I18N = {
hint_eff:"नोड का रंग प्रभावी स्थिति दिखाता है (निर्भरताओं सहित); यदि अपनी स्थिति आगे है, तो वह नीचे-बाएँ चिह्न के रूप में दिखती है।", hint_eff:"नोड का रंग प्रभावी स्थिति दिखाता है (निर्भरताओं सहित); यदि अपनी स्थिति आगे है, तो वह नीचे-बाएँ चिह्न के रूप में दिखती है।",
hint_desc:"विवरण: नोड के नीचे \" पंक्ति; --- के बाद #id ब्लॉक में लंबा पाठ — दोनों टूलटिप में (”)।", hint_desc:"विवरण: नोड के नीचे \" पंक्ति; --- के बाद #id ब्लॉक में लंबा पाठ — दोनों टूलटिप में (”)।",
hint_fold:"फ़ोल्डिंग: - [x] > … समेटा हुआ खुलता है, < वापस लाता है; नोड पर ▾/▸ टॉगल करता है (कीबोर्ड: ←/→)।", hint_fold:"फ़ोल्डिंग: - [x] > … समेटा हुआ खुलता है, < वापस लाता है; नोड पर ▾/▸ टॉगल करता है (कीबोर्ड: ←/→)।",
hint_jump:"किसी नोड पर Alt+क्लिक (टच पर लंबा दबाव) टेक्स्ट में उसकी पंक्ति पर ले जाता है; टेक्स्ट में Alt+क्लिक उस नोड को आरेख में दिखाता है।" hint_jump:"किसी नोड पर Alt+क्लिक (टच पर लंबा दबाव) टेक्स्ट में उसकी पंक्ति पर ले जाता है; टेक्स्ट में Alt+क्लिक उस नोड को आरेख में दिखाता है; :#id निर्भरता पर Ctrl+क्लिक उस ID की पंक्ति पर ले जाता है।"
}, },
zh: { zh: {
subtitle:"Werkbaum WBS / Lean Pathfinding · 项目结构编辑器(也支持功能树和需求树)", subtitle:"Werkbaum WBS / Lean Pathfinding · 项目结构编辑器(也支持功能树和需求树)",
@@ -2948,7 +2971,7 @@ const I18N = {
hint_eff:"节点颜色显示实际状态(含依赖);若自身状态更靠前,会以左下角标记显示。", hint_eff:"节点颜色显示实际状态(含依赖);若自身状态更靠前,会以左下角标记显示。",
hint_desc:"描述:节点下方的 \" 行;--- 之后的缩进 #id 块为长文本——均显示在提示中(”)。", hint_desc:"描述:节点下方的 \" 行;--- 之后的缩进 #id 块为长文本——均显示在提示中(”)。",
hint_fold:"折叠:- [x] > … 打开时即折叠,< 将其展开;节点上的 ▾/▸ 切换(键盘:←/→)。", hint_fold:"折叠:- [x] > … 打开时即折叠,< 将其展开;节点上的 ▾/▸ 切换(键盘:←/→)。",
hint_jump:"Alt+点击节点(触摸屏为长按)可跳转到文本中对应的行;在文本中 Alt+点击则把该节点带入视野。" hint_jump:"Alt+点击节点(触摸屏为长按)可跳转到文本中对应的行;在文本中 Alt+点击则把该节点带入视野;在依赖 :#id 上 Ctrl+点击可跳转到该 ID 所在的行。"
}, },
ja: { ja: {
subtitle:"Werkbaum WBS / Lean Pathfinding · プロジェクト構造エディター(フィーチャーツリーと要件ツリーにも対応)", subtitle:"Werkbaum WBS / Lean Pathfinding · プロジェクト構造エディター(フィーチャーツリーと要件ツリーにも対応)",
@@ -3052,7 +3075,7 @@ const I18N = {
hint_eff:"ノードの色は実効ステータス(依存関係込み)を示します。自身が先行している場合は左下のマークで表示されます。", hint_eff:"ノードの色は実効ステータス(依存関係込み)を示します。自身が先行している場合は左下のマークで表示されます。",
hint_desc:"説明:ノード直下の \" 行。--- 以降は #id ブロック(字下げ)で長文 — どちらもツールチップに表示(”)。", hint_desc:"説明:ノード直下の \" 行。--- 以降は #id ブロック(字下げ)で長文 — どちらもツールチップに表示(”)。",
hint_fold:"折りたたみ:- [x] > … は折りたたんだ状態で開き、< は呼び戻します。ノードの ▾/▸ で切替(キー:←/→)。", hint_fold:"折りたたみ:- [x] > … は折りたたんだ状態で開き、< は呼び戻します。ノードの ▾/▸ で切替(キー:←/→)。",
hint_jump:"ノードを Alt+クリック(タッチでは長押し)すると、テキストの該当行へ移動します。テキスト内で Alt+クリックすると、そのノードが図の中央に表示されます。" hint_jump:"ノードを Alt+クリック(タッチでは長押し)すると、テキストの該当行へ移動します。テキスト内で Alt+クリックすると、そのノードが図の中央に表示されます。依存関係 :#id を Ctrl+クリックすると、その ID の行へ移動します。"
} }
}; };
let lang = 'de'; let lang = 'de';
+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 Eingabehilfen wie die ID-Kurzform (D55), keine Notation: Der Parser sieht
sieht nie etwas davon, SPEC und llms.md bleiben unberührt. Hier steht, WAS 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 gilt — wann ein `:#…`-Kontext vorliegt, welche IDs dazu passen und welche
verdrahtet nur (Popup, Tasten, Einfügen). Frontend-Hausregel: Was ID unter der Schreibmarke steht; app.js verdrahtet nur (Popup, Tasten,
entscheidbar ist, gehört in ein Modul (D54-Nachtrag 3). */ Einfügen, Sprung). Frontend-Hausregel: Was entscheidbar ist, gehört in ein
Modul (D54-Nachtrag 3). */
const ID_CHARS = '[\\p{L}\\p{N}._-]'; const ID_CHARS = '[\\p{L}\\p{N}._-]';
@@ -69,6 +71,64 @@ export function collectIds(roots){
return out; 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 /* Passende Kandidaten: erst Präfix-Treffer, dann Teilstring-Treffer, je in
Dokumentreihenfolge; Groß-/Kleinschreibung egal (die IDs selbst bleiben, Dokumentreihenfolge; Groß-/Kleinschreibung egal (die IDs selbst bleiben,
wie sie geschrieben sind). Leeres Fragment (direkt nach `:#`) zeigt alle. */ wie sie geschrieben sind). Leeres Fragment (direkt nach `:#`) zeigt alle. */
+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);
});
});