feat(editor): ID-Kurzschreibweise #.kc als Eingabehilfe

Gefragt war, ob `#.kc` unter `#prod-stage` als Notation `#prod-stage.kc`
bedeuten könnte. Als Notation wäre es falsch, und das zeigt der eigene Plan:
181 IDs im Baumteil, davon 50 (28 %), deren gepunktetes Präfix gar nicht die
Eltern-ID ist (`#ed.parser` hängt unter `#ed.live`) — die Punkte benennen den
BEREICH, nicht den Pfad, genau wie D48 es vorsieht. Dazu: der
Beschreibungsteil hinter `---` hat keinen Baum (zwei Schreibweisen für eine
ID), Einrücken änderte die Identität und bräche `:#…`-Verweise, und aus einer
Konvention würde Grammatik. Gewinn wären 802 Zeichen in 40 kB.

Also als Eingabehilfe: getippt wird `#.kc`, beim Verlassen der Zeile steht
`#prod-stage.kc` im Text — derselbe Griff, mit dem das Umklappen im Diagramm
seine Faltmarke zurückschreibt (D38-Nachtrag 2). Die Datei bleibt eindeutig,
durchsuchbar und umsortierbar; llms.md ist unberührt, weil sich an der
Notation nichts ändert.

- parser.js: `expandShortIds(text)` neben `setFoldMark`, 17 Tests.
- app.js: löst beim Zeilenwechsel auf, aber nur in der Zeile, in der auch
  getippt wurde — ein bloß durchgeklicktes fremdes Dokument bleibt
  zeichengenau stehen (sonst fiele es aus dem Nachziehen, D27).

Falle, zum zweiten Mal dieselbe: `execCommand` verweigert re-entrant im
`input`-Ereignis den Dienst, `replaceTextUndoable` fällt dann auf
`src.value =` zurück und löscht die Undo-Historie. Gemessen: erstes
Rückgängig ohne Wirkung, jedes weitere `false`. Jetzt einen Zug später
(`setTimeout 0`) und nur mit Fokus im Textfeld; danach nimmt ein Rückgängig
genau die Auflösung zurück.

SPEC §9 + D55; Plan: #ed.shortid [x] (159 Knoten, 0 Warnungen).
288 Tests grün (271+17).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-24 11:47:16 +02:00
co-authored by Claude Opus 5
parent e937e1bbbb
commit 285d86a2e1
6 changed files with 310 additions and 2 deletions
+39 -2
View File
@@ -1,5 +1,5 @@
import './style.css';
import { parse, setFoldMark } from './parser.js';
import { parse, setFoldMark, expandShortIds } 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';
@@ -1348,11 +1348,48 @@ function caretLineOf(){
function syncCaret(){
const line = caretLineOf();
const moved = line !== caretLine;
caretLine = line;
if(moved) resolveShortId(caretLine);
caretLine = caretLineOf(); /* neu lesen: das Auflösen kann den Text ändern */
highlightCurrentNode(moved);
}
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.
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
bloß durchklickt, darf es nicht umgeschrieben bekommen (und damit aus dem
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(); });
function resolveShortId(line){
if(src.readOnly || 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
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.
Nur mit Fokus im Textfeld: Wer die Zeile per Klick ins Diagramm verlässt,
soll nicht zurückgerissen werden (`replaceTextUndoable` fokussiert selbst).
Die Kurzform bleibt dann stehen — sie ist eine gültige ID, es geht nichts
verloren, und beim nächsten Bearbeiten der Zeile wird sie aufgelöst. */
setTimeout(() => {
if(document.activeElement !== src) return;
const lines = src.value.split('\n');
const neu = expandShortIds(src.value).split('\n');
if(line > lines.length || neu[line-1] === lines[line-1]) return;
lines[line-1] = neu[line-1];
replaceTextUndoable(lines.join('\n'));
}, 0);
}
/* Gegenstück zum Alt+Klick am Knoten (D25, Nachtrag): Alt+Klick im Textfeld —
Tastatur Alt+Enter — holt den Knoten der Cursor-Zeile in die **Mitte** des
Diagramms und gibt ihm den Fokus. Der gewöhnliche Klick markiert ihn zwar