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:
co-authored by
Claude Opus 5
parent
e937e1bbbb
commit
285d86a2e1
+39
-2
@@ -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
|
||||
|
||||
@@ -36,6 +36,59 @@ export function setFoldMark(line, mark){
|
||||
return m[1] + m[2] + (mark ? mark + ' ' : '') + line.slice(m[0].length);
|
||||
}
|
||||
|
||||
const RE_LINE = /^([ \t]*)([-|+]|=(?=[ \t]))?\s*(?:([><])(?=[ \t])\s*)?(?:\[([^\]])\]\s*)?(?:([><])(?=[ \t])\s*)?(.*)$/;
|
||||
const RE_ID_TOKEN = /(^|\s)#([\p{L}\p{N}._-]+)/u;
|
||||
|
||||
/* Kurzschreibweise der Knoten-ID auflösen: `#.kc` unter `#prod-stage` wird zu
|
||||
`#prod-stage.kc` (D55). Das ist eine **Eingabehilfe**, keine Notation — die
|
||||
Datei enthält am Ende immer die volle ID. Deshalb steht sie hier als
|
||||
Text→Text-Funktion neben `setFoldMark`: Der Editor ruft sie beim Verlassen
|
||||
der Zeile auf und schreibt das Ergebnis zurück, wie das Umklappen im
|
||||
Diagramm seine Faltmarke zurückschreibt (D38-Nachtrag 2).
|
||||
|
||||
Aufgelöst wird gegen den **nächsten Vorfahren mit ID** — nicht zwingend den
|
||||
direkten Elternknoten, der kann selbst ohne ID sein. Findet sich keiner
|
||||
(Wurzelzeile) oder trägt er selbst noch eine Kurzform, bleibt die Zeile
|
||||
unangetastet: Lieber `#.kc` stehen lassen, als etwas Falsches hineinschreiben.
|
||||
Der Beschreibungsteil hinter `---` hat keinen Baum und wird nicht angefasst. */
|
||||
export function expandShortIds(text){
|
||||
const lines = text.split('\n');
|
||||
const stack = []; /* {width, id} — auch Knoten OHNE ID stehen drin */
|
||||
let changed = false;
|
||||
for(let i = 0; i < lines.length; i++){
|
||||
const raw = lines[i];
|
||||
if(/^\s*-{3,}\s*$/.test(raw)) break;
|
||||
const k = raw.indexOf('%%');
|
||||
const head = k === -1 ? raw : raw.slice(0, k);
|
||||
const tail = k === -1 ? '' : raw.slice(k);
|
||||
const m = head.match(RE_LINE);
|
||||
if(!m) continue;
|
||||
const body = m[6];
|
||||
if(!body.trim()) continue; /* leer oder nur Kommentar */
|
||||
if(/^"(\s|$)/.test(body)) continue; /* Beschreibungszeile, kein Knoten */
|
||||
const width = m[1].replace(/\t/g, ' ').length;
|
||||
while(stack.length && stack[stack.length - 1].width >= width) stack.pop();
|
||||
const t = body.match(RE_ID_TOKEN);
|
||||
let id = t ? t[2] : null;
|
||||
/* `.kc` ja, `..kc` nein — zwei Punkte sind keine vereinbarte Bedeutung. */
|
||||
if(id && /^\.[^.]/.test(id)){
|
||||
let anc = null;
|
||||
for(let j = stack.length - 1; j >= 0; j--){
|
||||
if(stack[j].id && stack[j].id[0] !== '.'){ anc = stack[j].id; break; }
|
||||
}
|
||||
if(anc){
|
||||
id = anc + id;
|
||||
lines[i] = head.slice(0, head.length - body.length)
|
||||
+ body.replace(RE_ID_TOKEN, (s, pre) => pre + '#' + id)
|
||||
+ tail;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
stack.push({width, id});
|
||||
}
|
||||
return changed ? lines.join('\n') : text;
|
||||
}
|
||||
|
||||
/* Status, die als „realisiert" zählen (XOR-Regel, SPEC §3/D35): Kosten sind
|
||||
investiert oder mehr. Absicht (`[?]`, `[ ]`, `[!]`), Ablehnung (`[-]`) und
|
||||
neutrale Knoten zählen nicht. */
|
||||
|
||||
Reference in New Issue
Block a user