feat: ID-Kurzform wird beim Doppelpunkt aufgelöst (D55-Nachtrag)
`#.kc` wird jetzt aufgelöst, sobald ein Doppelpunkt die ID abschließt — beim Tippen des Doppelpunkts ebenso wie beim nächsten Tastendruck in einer Zeile, die ihn schon trägt. Ohne Doppelpunkt bleibt es beim Verlassen der Zeile. Der Cursor allein löst weiterhin nichts aus: Angefasst wird nur die Zeile, in der auch getippt wurde (D55/D27). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a60c466d82
commit
4900eb3428
+1
-1
@@ -28,7 +28,7 @@ reverse.
|
||||
- A warning triangle replaces the question mark as the pointer over faulty line numbers
|
||||
- The node window replaces the browser tooltip everywhere — at the pointer, on keyboard focus and on touch
|
||||
- A `#` button in the diagram header puts node IDs in front of the titles
|
||||
- Typing `#.kc` under `#prod-stage` now expands to `#prod-stage.kc` when you leave the line
|
||||
- Typing `#.kc` under `#prod-stage` expands to `#prod-stage.kc`, right when you close the id with a colon
|
||||
- A camera button next to the history saves a snapshot on demand
|
||||
- Fix: the manual snapshot button confirmed without saving anything while nothing had changed yet
|
||||
- Warning line numbers carry their message as a tooltip
|
||||
|
||||
@@ -4006,6 +4006,66 @@ und Faltmarke, und dass das Ergebnis denselben Baum ergibt wie die von Hand
|
||||
ausgeschriebene Fassung). Das Zusammenspiel mit dem Textfeld bleibt
|
||||
Browser-Sache — die Lehre aus D54-Nachtrag 3.
|
||||
|
||||
**Nachtrag — aufgelöst wird schon beim Doppelpunkt, nicht erst beim Verlassen
|
||||
der Zeile.** Gefragt war, ob es „schon in dem Moment" gehen könnte, „wo ein `:`
|
||||
geschrieben wird oder der Cursor hinter einem vorhandenen `:` in der Zeile
|
||||
landet". Beides ist im Kern **derselbe** Anlass, und er ist besser als der
|
||||
bisherige: **Der Doppelpunkt schließt die ID ab.** Er ist per D36 der übliche
|
||||
Trenner vor dem Titel und zugleich der Anfang einer Abhängigkeitsliste
|
||||
(`#.kc:#db`) — steht er da, kann sich der Name der ID durch nichts mehr ändern,
|
||||
was danach getippt wird. Das Verlassen der Zeile war nie das eigentliche
|
||||
Kriterium, sondern nur der späteste Zeitpunkt, zu dem man sicher sein kann.
|
||||
|
||||
**Der Gewinn ist nicht Bequemlichkeit, sondern Nachprüfbarkeit.** Aufgelöst
|
||||
wird gegen den **Baum**-Vorfahren, und der ist nach D48 nicht immer der, dessen
|
||||
Namen das gepunktete Präfix trägt — im eigenen Plan bei **50 von 181 IDs
|
||||
(28 %)** nicht (gemessen oben). Wer das Ergebnis erst zwei Zeilen später bekommt, sieht es
|
||||
womöglich gar nicht mehr; wer es beim Doppelpunkt bekommt, liest es dort, wo
|
||||
sein Blick ohnehin steht, und kann es sofort hinschreiben, wie er es will.
|
||||
|
||||
**Eine Regel, zwei Auslöser.** Der Doppelpunkt wird entweder gerade getippt
|
||||
oder er steht schon da; im zweiten Fall greift es beim **nächsten
|
||||
Tastendruck** in dieser Zeile. Formuliert ist es deshalb als Zustand („die
|
||||
Kurzform ist abgeschlossen"), nicht als zwei Ereignisse.
|
||||
|
||||
**Der Cursor allein genügt nicht — bewusst gegen den Wortlaut der Frage.**
|
||||
„Der Cursor landet hinter einem `:`" wäre der dritte Auslöser gewesen und
|
||||
hätte die D55-Regel gebrochen, die genau hier steht: Angefasst wird nur die
|
||||
Zeile, **in der auch getippt wurde**. Ohne die Einschränkung schriebe bloßes
|
||||
Durchklicken ein fremdes Dokument um — es fiele damit aus dem Nachziehen
|
||||
mitgelieferter Fassungen (D27) und zeigte fortan „Original wiederherstellen".
|
||||
Ein Klick ist keine Absicht, ein Tastendruck ist eine.
|
||||
|
||||
**Umgesetzt als Vorfilter, nicht als zweite Regel-Instanz.**
|
||||
`shortIdClosed(zeile)` in `parser.js` beantwortet nur die billige Frage („folgt
|
||||
der Kurzform unmittelbar ein `:`?"), damit nicht bei jedem Tastendruck der
|
||||
ganze Text durchgesehen wird. Ob die Zeile überhaupt einen Knoten trägt und ob
|
||||
es einen Vorfahren mit ID gibt, weiß weiterhin allein `expandShortIds()` —
|
||||
dieselbe Haltung wie bei `initialCollapsed()` (D38-Nachtrag 2): eine Stelle
|
||||
kennt die Bedeutung, alles andere fragt sie. Scheitert die Auflösung (noch kein
|
||||
Vorfahr mit ID), bleibt die Zeile stehen und der nächste Tastendruck versucht
|
||||
es erneut; das Verlassen der Zeile fängt es ohnehin auf.
|
||||
|
||||
`writeShortId()` ist aus `resolveShortId()` herausgelöst, weil der neue Weg die
|
||||
Prüfung „ist das die getippte Zeile?" nicht braucht — er hängt am
|
||||
`input`-Ereignis, ist also per Definition darin. Das `setTimeout(…, 0)` bleibt
|
||||
für **beide** Wege nötig, und beim Doppelpunkt sogar offensichtlicher als
|
||||
vorher: Der Aufruf steckt jetzt unmittelbar im `input`-Ereignis, und
|
||||
`execCommand` verweigert dort den Dienst — `replaceTextUndoable` fiele auf
|
||||
`src.value =` zurück und löschte die Undo-Historie (D38-Nachtrag 2, D53).
|
||||
|
||||
**Nachgemessen** mit echten Tastendrücken: ` - #.kc` + `:` ergibt sofort
|
||||
` - #prod-stage.kc:`, die Schreibmarke steht unmittelbar hinter dem
|
||||
Doppelpunkt; ` Keycloak` tippt sich normal weiter, und zweimal Rückgängig nimmt
|
||||
erst den Titel, dann die Auflösung zurück (die Historie lebt also). In eine
|
||||
Zeile mit fertigem `#.kc: Keycloak` **hineinzuklicken** ändert nichts; **ein**
|
||||
Tastendruck darin löst auf, und das getippte Zeichen bleibt an seiner Stelle
|
||||
(`Keycloa!k`). Ohne Vorfahren mit ID bleibt `#.kc: …` über mehrere
|
||||
Tastendrücke stehen und wird aufgelöst, sobald es einen gibt. Zehn neue
|
||||
Zusicherungen für den Vorfilter; Gegenprobe: Lockert man ihn auf „irgendwo ein
|
||||
`:`", fällt genau die danach benannte, nimmt man die Punkt-Prüfung heraus,
|
||||
fallen genau die drei anderen.
|
||||
|
||||
## D56 — `#`-Umschalter: die Knoten-ID vor dem Titel, geschrieben wie im Text
|
||||
Die ID ist die **Adresse** eines Knotens — Ziel der Abhängigkeiten (§1), Schlüssel
|
||||
der Beschreibungsblöcke, und seit D55 auch das, was man beim Tippen abkürzt. Im
|
||||
|
||||
+14
-7
@@ -813,18 +813,25 @@ ohnehin über das `aria-label`. Siehe D56.
|
||||
|
||||
### Kurzschreibweise der Knoten-ID beim Tippen (`#.name`)
|
||||
Eine **Eingabehilfe**, keine Notation: Wer `#.kc` unter einem Knoten mit der ID
|
||||
`#prod-stage` schreibt, findet beim **Verlassen der Zeile** `#prod-stage.kc` im
|
||||
Text. Aufgelöst wird gegen den nächsten Vorfahren **mit** ID; gibt es keinen
|
||||
oder trägt er selbst noch eine Kurzform, bleibt die Zeile stehen (`#.kc` ist
|
||||
ohnehin eine gültige ID — der Punkt gehört zur Zeichenmenge, §1). `#..x` wird
|
||||
nicht angefasst.
|
||||
`#prod-stage` schreibt, findet `#prod-stage.kc` im Text. Aufgelöst wird gegen
|
||||
den nächsten Vorfahren **mit** ID; gibt es keinen oder trägt er selbst noch eine
|
||||
Kurzform, bleibt die Zeile stehen (`#.kc` ist ohnehin eine gültige ID — der
|
||||
Punkt gehört zur Zeichenmenge, §1). `#..x` wird nicht angefasst.
|
||||
|
||||
**Aufgelöst wird, sobald die ID abgeschlossen ist** — also sobald ihr
|
||||
unmittelbar ein **Doppelpunkt** folgt (`#.kc:`), der übliche Trenner vor dem
|
||||
Titel (§1) und zugleich der Anfang einer Abhängigkeitsliste. Das gilt beim
|
||||
Tippen des Doppelpunkts ebenso wie beim nächsten Tastendruck in einer Zeile,
|
||||
die ihn schon trägt. Ohne Doppelpunkt bleibt es beim **Verlassen der Zeile** —
|
||||
spätestens dann ist die ID fertig.
|
||||
|
||||
Die Datei enthält danach immer die volle ID: Sie bleibt durchsuchbar und
|
||||
überlebt das Umsortieren, worauf die Abhängigkeiten `:#…` bauen. **An der
|
||||
Notation ändert sich dadurch nichts** — der Parser sieht nie eine Kurzform,
|
||||
und `llms.md` (§13) bleibt unberührt. Angefasst wird nur die Zeile, in der auch
|
||||
getippt wurde; Abhängigkeiten (`:#…`) und der Beschreibungsteil hinter `---`
|
||||
bleiben außen vor. Siehe D55.
|
||||
**getippt** wurde — den Cursor hineinzusetzen genügt nicht, sonst schriebe
|
||||
bloßes Durchklicken ein fremdes Dokument um; Abhängigkeiten (`:#…`) und der
|
||||
Beschreibungsteil hinter `---` bleiben außen vor. Siehe D55.
|
||||
|
||||
### Zeilennummern im Texteditor
|
||||
Links neben dem Textfeld steht ein schmaler Streifen mit den **Zeilennummern** —
|
||||
|
||||
+34
-10
@@ -1,5 +1,5 @@
|
||||
import './style.css';
|
||||
import { parse, setFoldMark, expandShortIds } from './parser.js';
|
||||
import { parse, setFoldMark, expandShortIds, shortIdClosed } from './parser.js';
|
||||
import { computeCheapPlan, freshProdSet, initialCollapsed, nodeKeys, effectiveStatus, atMostM } from './model.js';
|
||||
import { esc, renderTreeHtml, TIP_RULE } from './render.js';
|
||||
import { formatWarning, warningText } from './warnings.js';
|
||||
@@ -1427,10 +1427,14 @@ function syncCaret(){
|
||||
}
|
||||
for(const ev of ['click','keyup','input','focus']) src.addEventListener(ev, syncCaret);
|
||||
|
||||
/* Kurzschreibweise der ID auflösen (D55): `#.kc` wird beim **Verlassen der
|
||||
Zeile** zu `#prod-stage.kc`. Eingabehilfe, keine Notation — in der Datei
|
||||
steht danach die volle ID, sie bleibt also durchsuchbar und überlebt das
|
||||
Umsortieren.
|
||||
/* Kurzschreibweise der ID auflösen (D55): `#.kc` wird zu `#prod-stage.kc`.
|
||||
Eingabehilfe, keine Notation — in der Datei steht danach die volle ID, sie
|
||||
bleibt also durchsuchbar und überlebt das Umsortieren.
|
||||
|
||||
**Wann:** sobald die ID abgeschlossen ist, also sobald ihr ein Doppelpunkt
|
||||
folgt (`#.kc:`) — beim Tippen des Doppelpunkts ebenso wie beim nächsten
|
||||
Tastendruck in einer Zeile, die ihn schon trägt. Sonst beim **Verlassen der
|
||||
Zeile**, spätestens dann ist sie fertig (D55-Nachtrag).
|
||||
|
||||
Angefasst wird nur die **eine** Zeile, in der auch getippt wurde. Beides ist
|
||||
nötig: `#.foo` ist schon heute eine gültige ID, und wer ein fremdes Dokument
|
||||
@@ -1438,13 +1442,33 @@ for(const ev of ['click','keyup','input','focus']) src.addEventListener(ev, sync
|
||||
Nachziehen mitgelieferter Fassungen fallen, D27). Geschrieben wird
|
||||
undo-fähig — ein Griff daneben kostet ein Strg+Z (D53). */
|
||||
let touchedLine = null;
|
||||
src.addEventListener('input', () => { touchedLine = caretLineOf(); });
|
||||
src.addEventListener('input', () => {
|
||||
touchedLine = caretLineOf();
|
||||
/* Der Vorfilter fragt nur die Zeile unter der Schreibmarke — ob wirklich
|
||||
etwas aufzulösen ist, entscheidet `expandShortIds()`. Scheitert es (noch
|
||||
kein Vorfahr mit ID), bleibt `touchedLine` stehen: Der nächste Tastendruck
|
||||
versucht es erneut, und das Verlassen der Zeile fängt es ohnehin auf. */
|
||||
if(shortIdClosed(caretLineText())) writeShortId(touchedLine);
|
||||
});
|
||||
/* Die Zeile unter der Schreibmarke, ohne den ganzen Text zu zerlegen — das
|
||||
liefe bei jedem Tastendruck über alle Zeilen. */
|
||||
function caretLineText(){
|
||||
const v = src.value, p = src.selectionStart;
|
||||
const s = v.lastIndexOf('\n', p - 1) + 1;
|
||||
const e = v.indexOf('\n', p);
|
||||
return e === -1 ? v.slice(s) : v.slice(s, e);
|
||||
}
|
||||
function resolveShortId(line){
|
||||
if(src.readOnly || line == null || line !== touchedLine) return;
|
||||
if(line == null || line !== touchedLine) return;
|
||||
touchedLine = null;
|
||||
/* **Nicht** sofort schreiben: Der Zeilenwechsel kommt oft aus dem
|
||||
`input`-Ereignis der Enter-Taste, und `execCommand` verweigert den Dienst,
|
||||
wenn es re-entrant darin aufgerufen wird. `replaceTextUndoable` fiele dann
|
||||
writeShortId(line);
|
||||
}
|
||||
function writeShortId(line){
|
||||
if(src.readOnly || line == null) return;
|
||||
/* **Nicht** sofort schreiben: Beide Wege hierher hängen am
|
||||
`input`-Ereignis — der Doppelpunkt unmittelbar, der Zeilenwechsel über die
|
||||
Enter-Taste —, und `execCommand` verweigert den Dienst, wenn es
|
||||
re-entrant darin aufgerufen wird. `replaceTextUndoable` fiele dann
|
||||
auf `src.value =` zurück — und das löscht die Undo-Historie (D38-Nachtrag
|
||||
2). Gemessen: erstes Rückgängig ohne Wirkung, jedes weitere `false`.
|
||||
Deshalb ein Zug später, wenn das Ereignis zugestellt ist.
|
||||
|
||||
@@ -131,6 +131,24 @@ export function expandShortIds(text){
|
||||
return changed ? lines.join('\n') : text;
|
||||
}
|
||||
|
||||
/* Ist die Kurzform in dieser Zeile schon **abgeschlossen**? Also folgt ihr
|
||||
unmittelbar ein Doppelpunkt — der übliche Trenner vor dem Titel (§1/D36) und
|
||||
zugleich der Anfang einer Abhängigkeitsliste (`#.kc:#db`). Dann steht fest,
|
||||
wie die ID heißt, und der Editor löst sofort auf, statt das Verlassen der
|
||||
Zeile abzuwarten (D55-Nachtrag).
|
||||
|
||||
Bewusst nur ein **Vorfilter**: Ob die Zeile überhaupt einen Knoten trägt und
|
||||
ob es einen Vorfahren mit ID gibt, weiß allein `expandShortIds()` — es bleibt
|
||||
die eine Stelle, die die Regel kennt. */
|
||||
export function shortIdClosed(line){
|
||||
const k = line.indexOf('%%');
|
||||
const head = k === -1 ? line : line.slice(0, k);
|
||||
const m = head.match(RE_ID_TOKEN);
|
||||
/* `.kc` ja, `..kc` nein — dieselbe Prüfung wie oben. */
|
||||
if(!m || !/^\.[^.]/.test(m[2])) return false;
|
||||
return head[m.index + m[0].length] === ':';
|
||||
}
|
||||
|
||||
/* Status, die als „realisiert" zählen (XOR-Regel, SPEC §3/D35): Kosten sind
|
||||
investiert oder mehr. Absicht (`[?]`, `[ ]`, `[!]`), Ablehnung (`[-]`) und
|
||||
neutrale Knoten zählen nicht. */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { expandShortIds, parse } from '../src/parser.js';
|
||||
import { expandShortIds, shortIdClosed, parse } from '../src/parser.js';
|
||||
|
||||
/* Kurzschreibweise der Knoten-ID (D55): `#.kc` unter `#prod-stage` wird beim
|
||||
Verlassen der Zeile zu `#prod-stage.kc`. Eingabehilfe, keine Notation — in
|
||||
@@ -115,3 +115,51 @@ describe('expandShortIds — das Ergebnis parst wie eine von Hand geschriebene I
|
||||
expect(r.warnings).toEqual([]); /* kein unknownDep mehr */
|
||||
});
|
||||
});
|
||||
|
||||
/* Vorfilter für den frühen Zeitpunkt (D55-Nachtrag): Folgt der Kurzform
|
||||
unmittelbar ein Doppelpunkt, steht fest, wie die ID heißt — der Editor löst
|
||||
dann sofort auf, statt das Verlassen der Zeile abzuwarten. */
|
||||
describe('shortIdClosed — die Kurzform ist mit einem Doppelpunkt abgeschlossen', () => {
|
||||
it('erkennt den Trenner vor dem Titel', () => {
|
||||
expect(shortIdClosed(' - #.kc: Keycloak')).toBe(true);
|
||||
});
|
||||
|
||||
it('erkennt ihn auch am Zeilenende, noch ohne Titel', () => {
|
||||
expect(shortIdClosed(' - #.kc:')).toBe(true);
|
||||
});
|
||||
|
||||
it('zählt auch den Anfang einer Abhängigkeitsliste', () => {
|
||||
expect(shortIdClosed(' - #.kc:#db')).toBe(true);
|
||||
});
|
||||
|
||||
it('sagt ohne Doppelpunkt nein — dort wird beim Verlassen der Zeile aufgelöst', () => {
|
||||
expect(shortIdClosed(' - #.kc Keycloak')).toBe(false);
|
||||
});
|
||||
|
||||
it('verlangt ihn unmittelbar, nicht irgendwo in der Zeile', () => {
|
||||
expect(shortIdClosed(' - #.kc Keycloak: Stage')).toBe(false);
|
||||
});
|
||||
|
||||
it('lässt die volle ID in Ruhe — da gibt es nichts aufzulösen', () => {
|
||||
expect(shortIdClosed(' - #prod-stage.kc: Keycloak')).toBe(false);
|
||||
});
|
||||
|
||||
it('fasst `#..x` nicht an, wie expandShortIds auch nicht', () => {
|
||||
expect(shortIdClosed(' - #..x: X')).toBe(false);
|
||||
});
|
||||
|
||||
it('sieht nur das erste `#`-Token', () => {
|
||||
expect(shortIdClosed(' - #a: siehe #.b: dort')).toBe(false);
|
||||
});
|
||||
|
||||
it('übergeht den Kommentar', () => {
|
||||
expect(shortIdClosed(' - #.kc Keycloak %% #.x: Notiz')).toBe(false);
|
||||
});
|
||||
|
||||
it('was er meldet, löst expandShortIds auch wirklich auf', () => {
|
||||
const zeile = ' - #.kc: Keycloak';
|
||||
expect(shortIdClosed(zeile)).toBe(true);
|
||||
expect(expandShortIds(['#prod-stage: Stage', zeile].join('\n')))
|
||||
.toContain('#prod-stage.kc: Keycloak');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user