docs(rfc): RFC 003 — Knoten im Text verschieben, als Bauanleitung für einen Coding-Agenten
Zwei Knöpfe in der Editor-Titelzeile und Alt+↑/↓ verschieben den Block des Knotens der Cursor-Zeile (Zeile, \-Fortsetzungen, "-Beschreibungen, Teilbaum) vor den vorherigen bzw. hinter den nächsten Geschwisterknoten — als reine Text→Text-Regel neben setFoldMark/expandShortIds, ein Undo-Schritt über writeAt, ohne neue Editor-Komponente. Das RFC legt Blockgrenzen, Nachbarsuche, Zusammensetzen, Verdrahtung, i18n, 15 Testfälle samt Mutations-Gegenproben, Browser-Nachmessung und die nachzuziehenden Dokumente (SPEC §9, CHANGELOG, D95, Plan-Knoten #ed.move) fest. Nichts gebaut. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
5e72cd873f
commit
736e9c1d88
@@ -0,0 +1,658 @@
|
||||
# RFC 003 — Knoten im Text verschieben: zwei Knöpfe, ein Block, ein Undo-Schritt
|
||||
|
||||
| | |
|
||||
|---------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Status | **Entschieden, nicht gebaut** (2026-09-03). Dieses Dokument ist die vollständige Bauanleitung; ein Coding-Agent soll es ohne Rückfrage umsetzen können. |
|
||||
| Anlass | Nutzerwunsch: In der Editor-Titelzeile Knöpfe „nach oben“/„nach unten“, die den Knoten der Cursor-Zeile vor den vorherigen bzw. hinter den nächsten Geschwisterknoten schieben — samt `"`-Zeilen und `\`-Fortsetzungen |
|
||||
| Plan-Knoten | `#ed.move` in `docs/examples/werkbaum.werkbaum` (anzulegen, §9.6) |
|
||||
| Entscheidung | D95 in `docs/DECISIONS.md` (anzulegen, §9.5) |
|
||||
| Berührt | `frontend/src/parser.js` (eine neue Text→Text-Funktion), `frontend/src/app.js` (zwei Knöpfe, Tastatur, Zustand), `frontend/index.html`, `frontend/src/style.css` (höchstens eine Regel), i18n ×9, `frontend/tests/move.test.js` (neu), SPEC §9 (ein Abschnitt), CHANGELOG, Plan |
|
||||
| Berührt nicht | Notation (kein neues Zeichen, `llms.md` bleibt unberührt), Renderer, Modell, Backend, Live-Protokoll (D76), Deploy |
|
||||
| Neue Technologie / Abhängigkeit | **keine** — reine Textoperation auf dem vorhandenen `<textarea>` |
|
||||
|
||||
## 1. Zusammenfassung
|
||||
|
||||
Der Text-Editor bekommt in seiner Titelzeile zwei Knöpfe (▲ / ▼) und die
|
||||
Tastenkürzel **Alt+↑** / **Alt+↓**. Sie verschieben den **Block** des
|
||||
Knotens, in dem die Schreibmarke steht, vor seinen vorherigen bzw. hinter
|
||||
seinen nächsten **Geschwister**-Block. Ein Block ist alles, was zu dem Knoten
|
||||
gehört: seine Zeile, ihre `\`-Fortsetzungen, seine `"`-Beschreibungszeilen
|
||||
und sein **ganzer Teilbaum** (jede Zeile darunter mit größerer Einrückung,
|
||||
samt deren Fortsetzungen, Beschreibungen und Kommentaren). Geschrieben wird
|
||||
als **ein Undo-Schritt** über den vorhandenen `execCommand`-Weg; die
|
||||
Schreibmarke wandert mit ihrem Knoten mit. Am Rand der Geschwistergruppe
|
||||
(kein Geschwister in dieser Richtung) sind Knopf und Taste wirkungslos, der
|
||||
Knopf ist dann deaktiviert.
|
||||
|
||||
Das braucht **keine neue Editor-Komponente** (kein CodeMirror, kein Monaco —
|
||||
D76 hält das als eigene Frage offen). Verschieben ist eine Text→Text-Regel,
|
||||
und für genau diese Sorte gibt es im Haus drei Vorbilder: `setFoldMark`
|
||||
(D38), `expandShortIds` (D55) und `setStatusBox` (D91-Nachtrag 8) in
|
||||
`parser.js`, alle headless getestet, alle über `app.js` undo-fähig ins
|
||||
Textfeld geschrieben.
|
||||
|
||||
## 2. Begriffe, wie der Code sie schon kennt
|
||||
|
||||
Wer das baut, sollte diese Stellen zuerst lesen:
|
||||
|
||||
- **`logicalLines(text)`** in `frontend/src/parser.js` (~Zeile 77): liefert
|
||||
je logischer Zeile `{raw, line, cont}` — `line` ist die Nummer der ersten
|
||||
Textzeile, `cont` die Nummern der angehängten `\`-Fortsetzungen (SPEC §1,
|
||||
D59). Hinter dem `---`-Trenner wird nicht mehr verbunden. Die Menge aller
|
||||
`cont`-Nummern ist genau die Menge der Zeilen, die **keine eigene Zeile**
|
||||
sind, sondern zu der davor gehören.
|
||||
- **`RE_CONT`**, **`RE_SEP`** (parser.js ~Zeile 60–63): Fortsetzungsmarke
|
||||
(Leerraum + `\` am Ende, Leerraum ist Pflicht) und `---`-Trenner.
|
||||
- **Einrückungsbreite**: `m[1].replace(/\t/g, ' ').length` (parser.js
|
||||
~Zeile 130 und 295) — Tab zählt als zwei Leerzeichen (SPEC §2).
|
||||
- **Beschreibungszeile (Kurzform)**: erstes Zeichen nach der Einrückung ist
|
||||
`"`, gefolgt von Leerraum oder Zeilenende, auf einer Zeile **ohne**
|
||||
Zerlegungszeichen (SPEC §1). Sie gehört zum vorangehenden Knoten.
|
||||
- **`node.line`** und **`node.descLines`** (parser.js ~Zeile 184 und 378):
|
||||
`line` ist die Nummer der Knotenzeile, `descLines` die Nummern aller
|
||||
Zeilen, die zu diesem Knoten gehören, ohne einen eigenen zu tragen —
|
||||
`"`-Zeilen, `\`-Fortsetzungen **und** die Zeilen seines ID-Blocks im
|
||||
`---`-Beschreibungsteil. Damit wird die Cursor-Zeile zum Knoten aufgelöst
|
||||
(§4.1).
|
||||
- **`replaceTextUndoable(neu)`** in `frontend/src/app.js` (~Zeile 1519) und
|
||||
**`writeAt(von, bis, ein, selA, selB)`** (~Zeile 1240): die beiden
|
||||
Schreibwege über `document.execCommand('insertText')`. `value =` und
|
||||
`setRangeText` **zerstören die Undo-Historie** (D38-Nachtrag 2, D53) und
|
||||
sind tabu. Für dieses Feature ist `writeAt` der richtige (§5.3).
|
||||
- **`viewOnly`** (app.js Zeile 135): Nur-Ansehen-Modus aus D94 — kein
|
||||
programmatischer Schreibzugriff; `replaceTextUndoable` prüft das selbst,
|
||||
`writeAt` **nicht**. Die Knöpfe müssen es selbst prüfen.
|
||||
- **`caretLineOf()`** (app.js ~Zeile 2816) und **`syncCaret()`** (~Zeile
|
||||
2819): Cursor-Zeile lesen; `syncCaret` läuft bei `click`, `keyup`, `input`,
|
||||
`focus` am Textfeld — dort hängt der Knopfzustand (§5.4) mit ein.
|
||||
- **`updateDocButtons()`** (app.js ~Zeile 5333): das Vorbild für Knöpfe der
|
||||
Editor-Titelzeile, die je nach Lage sichtbar/deaktiviert sind.
|
||||
- **`keyboardOnJump(true)`** (app.js ~Zeile 1448): hält auf dem Telefon die
|
||||
Bildschirmtastatur unten, wenn das Textfeld programmatisch fokussiert wird
|
||||
(D25); `scrollEditorToOffset(offset)` (~Zeile 1419) scrollt eine Textstelle
|
||||
per Spiegel-`div` in Sicht.
|
||||
- **Editor-Titelzeile**: `frontend/index.html` ~Zeile 106–137, die Gruppe
|
||||
`<span class="standgroup">` mit `saveBtn`, `snapAddBtn`, `snapBtn`,
|
||||
`reloadBtn`, `shareBtn`. Knöpfe tragen `class="copybtn"`, ein Inline-SVG
|
||||
(24×24, `stroke="currentColor" stroke-width="1.8"`) und
|
||||
`data-i18n-title`/`data-i18n-aria`.
|
||||
- **i18n**: `const I18N = {de:{…}, en:{…}, es:{…}, fr:{…}, pl:{…}, ru:{…},
|
||||
hi:{…}, zh:{…}, ja:{…}}` in app.js (~Zeile 3468 ff.), Deutsch ist die
|
||||
Quellsprache. Die Legende endet mit
|
||||
`<div class="hint-op">${esc(t('hint_jump'))}<br>${esc(t('hint_save'))}</div>`
|
||||
(~Zeile 4787) — dort kommt die neue Bedienungs-Zeile dazu.
|
||||
**Anführungszeichen in i18n-Texten nur typografisch** (`„…“`, `“…”`,
|
||||
`«…»`), ein gerades `"` bricht den Bundle, und `npm test` merkt es nicht
|
||||
(D91-Nachtrag 8).
|
||||
|
||||
## 3. Was ein „Knoten“ beim Verschieben umfasst
|
||||
|
||||
Verschoben wird der **Block** eines Knotens. Der Block ist eine zusammen-
|
||||
hängende Folge von Textzeilen im **Baumteil** (vor dem `---`-Trenner):
|
||||
|
||||
1. Die **Knotenzeile** `N` (die Zeile, die `node.line` nennt).
|
||||
2. Danach jede Zeile, solange sie eine der folgenden ist:
|
||||
- eine **Fortsetzungszeile** (steht in einer `cont`-Liste von
|
||||
`logicalLines()`) — egal, wie sie eingerückt ist;
|
||||
- eine **`"`-Zeile** (Beschreibung, Kurzform) — egal, wie sie eingerückt
|
||||
ist;
|
||||
- eine **Leerzeile** oder eine Zeile, die nur aus Leerraum und einem
|
||||
`%%`-Kommentar besteht;
|
||||
- eine **Knotenzeile mit größerer Einrückungsbreite** als `N` (ein
|
||||
Nachkomme) — samt allem, was nach diesen Regeln zu ihr gehört.
|
||||
3. Der Block endet **vor** der ersten Zeile, die nichts davon ist: die
|
||||
nächste Knotenzeile mit Einrückungsbreite **≤** der von `N`, der
|
||||
`---`-Trenner oder das Dateiende.
|
||||
4. **Nachlaufende** Leer- und Kommentar-only-Zeilen gehören **nicht** zum
|
||||
Block — sie sind der **Abstand** zwischen zwei Blöcken und bleiben beim
|
||||
Verschieben zwischen den beiden Blöcken stehen (§4.3). Leer- und
|
||||
Kommentarzeilen **innerhalb** des Blocks (zwischen zwei Nachkommen)
|
||||
wandern mit.
|
||||
|
||||
**Was nicht mitwandert und nicht angefasst wird:**
|
||||
|
||||
- Der **Beschreibungsteil** hinter `---`: ID-Blöcke sind per ID adressiert,
|
||||
nicht per Position. Sie bleiben, wo sie sind.
|
||||
- Alles **außerhalb** der beiden getauschten Blöcke und ihres Abstands. Die
|
||||
Operation ist ein reiner Tausch zweier Zeilenfolgen; die Gesamtlänge des
|
||||
Textes ändert sich **nicht** — jedes Zeichen vor und nach dem geänderten
|
||||
Bereich behält seinen Offset (das macht die Schreibmarken-Korrektur trivial,
|
||||
§5.3).
|
||||
|
||||
**Geschwister** sind Knotenzeilen mit **derselben** Einrückungsbreite unter
|
||||
demselben Elternknoten — also ohne dass zwischen ihnen eine Knotenzeile mit
|
||||
**kleinerer** Breite steht. Wurzelknoten (Breite 0, SPEC §2) sind
|
||||
untereinander Geschwister; die Regel ist dieselbe.
|
||||
|
||||
## 4. Die Regel als Funktion (`parser.js`)
|
||||
|
||||
### 4.1 Signatur
|
||||
|
||||
```js
|
||||
/* Verschiebt den Block des Knotens der Zeile `line` um eine Position unter
|
||||
seinen Geschwistern. `dir` ist -1 (nach oben) oder +1 (nach unten).
|
||||
Liefert null, wenn nichts zu tun ist (Zeile trägt keinen Knoten, kein
|
||||
Geschwister in dieser Richtung, Zeile liegt hinter `---`); sonst
|
||||
{text, from, to, lines, shift}:
|
||||
text — der neue Gesamttext
|
||||
from — 0-basierter Index der ersten geänderten Zeile
|
||||
to — 0-basierter Index der letzten geänderten Zeile (einschließlich)
|
||||
lines — die neuen Zeilen des Bereichs [from, to]
|
||||
shift — um wie viele Zeilen die Knotenzeile gewandert ist (negativ = hoch) */
|
||||
export function moveNodeBlock(text, line, dir)
|
||||
```
|
||||
|
||||
`line` ist 1-basiert wie überall im Projekt (`node.line`, Warnungen,
|
||||
`caretLine`). Die Funktion ist **rein** (kein DOM, kein Zustand) und liegt
|
||||
neben `setFoldMark`, `setStatusBox` und `expandShortIds` in `parser.js`.
|
||||
|
||||
Zusätzlich, für den Knopfzustand ohne Schreiben:
|
||||
|
||||
```js
|
||||
/* Sagt, ob der Knoten der Zeile `line` ein Geschwister vor (`up`) bzw.
|
||||
nach (`down`) sich hat. Ohne Knoten an der Zeile: {up:false, down:false}. */
|
||||
export function moveTargets(text, line)
|
||||
```
|
||||
|
||||
Beide teilen sich eine interne Hilfsfunktion, die die Blockgrenzen und die
|
||||
Nachbarblöcke bestimmt (§4.3); `moveNodeBlock` setzt danach nur noch zusammen.
|
||||
|
||||
### 4.2 Die Cursor-Zeile zum Knoten auflösen
|
||||
|
||||
Die Schreibmarke kann auf einer Zeile stehen, die keinen eigenen Knoten
|
||||
trägt, aber zu einem gehört (`"`-Zeile, Fortsetzung, Zeile eines ID-Blocks
|
||||
hinter `---`). SPEC §9 sagt: Dann gilt dieser Knoten als ausgewählt — und
|
||||
der Nutzer, der dort tippt, arbeitet an genau diesem Knoten. Dieselbe Lesart
|
||||
gilt hier.
|
||||
|
||||
Auflösung: `parse(text).roots` rekursiv durchlaufen; der Knoten mit
|
||||
`node.line === line` oder `node.descLines?.includes(line)` ist der gemeinte;
|
||||
`N = node.line`. Gibt es keinen (Kommentarzeile, Leerzeile, `---` selbst,
|
||||
Zeile hinter `---` ohne Block-Zuordnung) → `null`.
|
||||
|
||||
Achtung: `parse()` rechnet mit der **kommentarfreien, verbundenen** Fassung;
|
||||
die Blockgrenzen (§4.3) werden aber auf den **Rohzeilen** `text.split('\n')`
|
||||
bestimmt. Das passt zusammen, weil `node.line` immer die Nummer der ersten
|
||||
Rohzeile ist (D59: „alles gehört zur ersten Zeile“).
|
||||
|
||||
### 4.3 Blockgrenzen und Nachbar bestimmen
|
||||
|
||||
Auf `lines = text.split('\n')` (0-basiert; Rohzeile `i` ist Textzeile `i+1`):
|
||||
|
||||
1. `sep` = Index der ersten Zeile, die `RE_SEP` erfüllt (auf der Rohzeile
|
||||
ohne Kommentar geprüft), sonst `lines.length`. Alles ab `sep` ist tabu.
|
||||
2. `contSet` = Menge aller `cont`-Nummern aus `logicalLines(text)`,
|
||||
umgerechnet auf 0-basierte Indizes.
|
||||
3. Klassifikation einer Rohzeile `i < sep`:
|
||||
- `strip = lines[i].replace(/%%.*$/, '')`
|
||||
- **cont**, wenn `contSet.has(i)`
|
||||
- **blank**, wenn `strip.trim() === ''` (deckt Leerzeile und
|
||||
Kommentar-only ab)
|
||||
- **desc**, wenn `/^[ \t]*"([ \t]|$)/.test(strip)`
|
||||
- sonst **node** mit `width = strip.match(/^[ \t]*/)[0].replace(/\t/g, ' ').length`
|
||||
|
||||
(Eine Zeile, die nach Zeichen und Statusbox kein Label hat, wäre laut
|
||||
SPEC §1 kein Knoten — für die Blockrechnung darf sie trotzdem als `node`
|
||||
mit ihrer Breite zählen; sie gehört dann zu dem, wo sie steht. Solche
|
||||
Zeilen kommen praktisch nicht vor.)
|
||||
4. **Blockende** `end(start)`: `j = start + 1`; solange `j < sep` und die
|
||||
Zeile `j` cont, blank, desc oder node mit `width > width(start)` ist,
|
||||
`j++`. Dann `j` rückwärts über blank-Zeilen zurückziehen (nachlaufende
|
||||
Abstände gehören nicht dazu). Ergebnis: letzter Index des Blocks
|
||||
(einschließlich), mindestens `start` plus seine eigenen cont-Zeilen.
|
||||
5. **Vorheriges Geschwister** `prevStart(N)`: `k = N - 1`; solange `k ≥ 0`:
|
||||
ist Zeile `k` cont, blank oder desc → `k--`; ist sie node mit `width >
|
||||
width(N)` → `k--` (ein Nachkomme des Vorgängers); ist sie node mit
|
||||
`width === width(N)` → **gefunden**, `P = k`; ist sie node mit `width <
|
||||
width(N)` → **kein** Vorgänger (Elternknoten erreicht) → `null`. Bei
|
||||
`k < 0` ebenso `null`.
|
||||
Vorsicht: Beim Rückwärtslaufen trifft man eine `desc`-Zeile oder eine
|
||||
`cont`-Zeile, **bevor** man deren Knotenzeile trifft. Deshalb werden sie
|
||||
übersprungen — die Zuordnung ergibt sich, sobald man die Knotenzeile
|
||||
erreicht. Der gefundene Vorgänger-Block ist `[P, end(P)]`; per Konstruktion
|
||||
ist `end(P) < N`.
|
||||
6. **Nächstes Geschwister** `nextStart(N)`: `k = end(N) + 1`; über
|
||||
blank-Zeilen vorwärts; die erste Nicht-blank-Zeile muss node mit `width
|
||||
=== width(N)` sein → `Q = k`; alles andere (`k ≥ sep`, node mit kleinerer
|
||||
Breite) → `null`. (Größere Breite, cont oder desc können hier nicht
|
||||
auftreten — sie wären Teil des Blocks.) Der Nachbar-Block ist
|
||||
`[Q, end(Q)]`.
|
||||
|
||||
### 4.4 Zusammensetzen
|
||||
|
||||
Nach oben (`dir = -1`, Vorgänger `[P, endP]`, eigener Block `[N, endN]`,
|
||||
Abstand `gap = lines.slice(endP + 1, N)`):
|
||||
|
||||
```
|
||||
neu = lines.slice(0, P)
|
||||
.concat(lines.slice(N, endN + 1)) // eigener Block zuerst
|
||||
.concat(gap) // Abstand bleibt zwischen beiden
|
||||
.concat(lines.slice(P, endP + 1)) // dann der Vorgänger
|
||||
.concat(lines.slice(endN + 1));
|
||||
from = P; to = endN; shift = P - N;
|
||||
```
|
||||
|
||||
Nach unten (`dir = +1`, Nachbar `[Q, endQ]`, `gap = lines.slice(endN + 1, Q)`):
|
||||
|
||||
```
|
||||
neu = lines.slice(0, N)
|
||||
.concat(lines.slice(Q, endQ + 1)) // Nachbar zuerst
|
||||
.concat(gap)
|
||||
.concat(lines.slice(N, endN + 1)) // dann der eigene Block
|
||||
.concat(lines.slice(endQ + 1));
|
||||
from = N; to = endQ; shift = (endQ - Q + 1) + gap.length;
|
||||
```
|
||||
|
||||
`text = neu.join('\n')`. Weil nur Zeilen umgestellt werden, ist
|
||||
`neu.length === lines.length` und `text.length === alt.length`.
|
||||
|
||||
### 4.5 Was ausdrücklich **nicht** passiert
|
||||
|
||||
- Keine Änderung an Einrückung, Zeichen, Statusbox, Faltmarke, IDs, Tags,
|
||||
Abhängigkeiten. Eine Faltmarke `>` wandert mit ihrer Zeile — die Faltung
|
||||
bleibt am Knoten.
|
||||
- Kein Verschieben **über** die Geschwistergruppe hinaus (nicht zum Onkel,
|
||||
nicht in einen anderen Elternknoten). Das wäre Umhängen, nicht
|
||||
Verschieben, und eine eigene Entscheidung.
|
||||
- Kein Verschieben, wenn die Zeile keinen Knoten trägt oder hinter `---`
|
||||
liegt, ohne zu einem ID-Block zu gehören.
|
||||
- Keine Prüfung der Gates: Wer in einer gemischten Gruppe verschiebt, kann
|
||||
das erste Kind wechseln und damit die Darstellung der Gruppe (SPEC §3:
|
||||
Darstellung nach dem ersten Kind). Das ist dieselbe Wirkung wie beim
|
||||
händischen Umsortieren; `mixedGate` warnt ohnehin.
|
||||
|
||||
## 5. Verdrahtung im Editor (`app.js`, `index.html`)
|
||||
|
||||
### 5.1 Zwei Knöpfe in der Editor-Titelzeile
|
||||
|
||||
In `index.html` innerhalb von `<span class="standgroup">`, **vor**
|
||||
`saveBtn` (die Verschiebe-Knöpfe gehören zum Bearbeiten, Speichern & Co. zur
|
||||
Ablage; so stehen sie am Anfang der Gruppe):
|
||||
|
||||
```html
|
||||
<!-- Knoten der Cursor-Zeile unter seinen Geschwistern verschieben (RFC 003,
|
||||
D95): der ganze Block — Zeile, Fortsetzungen, "-Zeilen, Teilbaum — als
|
||||
ein Undo-Schritt. Deaktiviert ohne Geschwister in der Richtung. -->
|
||||
<button type="button" class="copybtn" id="moveUpBtn" data-i18n-title="moveUpTooltip" data-i18n-aria="moveUpTooltip" title="Knoten nach oben (Alt+↑)" aria-label="Knoten nach oben (Alt+↑)" disabled>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V6"/><path d="M6 12l6-6 6 6"/></svg>
|
||||
</button>
|
||||
<button type="button" class="copybtn" id="moveDownBtn" data-i18n-title="moveDownTooltip" data-i18n-aria="moveDownTooltip" title="Knoten nach unten (Alt+↓)" aria-label="Knoten nach unten (Alt+↓)" disabled>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v13"/><path d="M6 12l6 6 6-6"/></svg>
|
||||
</button>
|
||||
```
|
||||
|
||||
**Eine CSS-Regel** in `frontend/src/style.css` neben den `.copybtn`-Regeln
|
||||
(geprüft: es gibt bisher keine allgemeine Regel für deaktivierte Knöpfe, der
|
||||
`reloadBtn` verlässt sich auf die Browser-Voreinstellung):
|
||||
|
||||
```css
|
||||
.copybtn:disabled{opacity:.45;cursor:default}
|
||||
```
|
||||
|
||||
Sie gilt damit auch dem `reloadBtn` — gewollt, derselbe Zustand soll gleich
|
||||
aussehen. Auf dem Telefon (D17, `body.mobile`) stehen die Knöpfe in der
|
||||
Text-Titelzeile wie die übrigen; nach dem Einbau bei 375 px nachmessen, dass
|
||||
die Zeile einreihig bleibt (Vorbild D17-Nachtrag 5 / D50 — die Umbruch-
|
||||
Schwelle liegt bei 440 px; die Editor-Titelzeile hat weniger Elemente als
|
||||
die Diagramm-Titelzeile und war bisher nie knapp).
|
||||
|
||||
### 5.2 Tastatur
|
||||
|
||||
Am Textfeld, neben den vorhandenen `keydown`-Handlern (~Zeile 2915):
|
||||
|
||||
```js
|
||||
/* Alt+↑ / Alt+↓ verschieben den Knoten der Cursor-Zeile (RFC 003). Ohne
|
||||
Alt gehören die Pfeile der Schreibmarke bzw. der ID-Vorschlagsliste (D63). */
|
||||
src.addEventListener('keydown', e => {
|
||||
if(!e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
|
||||
if(e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return;
|
||||
e.preventDefault();
|
||||
moveCaretNode(e.key === 'ArrowUp' ? -1 : 1);
|
||||
});
|
||||
```
|
||||
|
||||
`e.repeat` **nicht** ausfiltern: gehaltenes Alt+↓ soll den Knoten
|
||||
weiterschieben, jeder Schritt ist ein eigener Undo-Schritt — das ist das
|
||||
Verhalten von IntelliJ und VS Code. Die ID-Vorschlagsliste (D63) fängt
|
||||
ArrowUp/Down nur **ohne** Alt ab; kein Konflikt.
|
||||
|
||||
### 5.3 Der Schreibvorgang
|
||||
|
||||
```js
|
||||
function moveCaretNode(dir){
|
||||
if(viewOnly) return; /* Nur-Ansehen (D94 §6.5) */
|
||||
const line = caretLineOf();
|
||||
const col = src.selectionStart - (src.value.lastIndexOf('\n', src.selectionStart - 1) + 1);
|
||||
const r = moveNodeBlock(src.value, line, dir);
|
||||
if(!r) return;
|
||||
const alt = src.value;
|
||||
/* Nur den geänderten Bereich ersetzen — die Zeilen [from, to]. */
|
||||
const von = offsetOfLine(alt, r.from); /* Offset des Zeilenanfangs von Rohzeile from */
|
||||
const bis = offsetOfLine(alt, r.to + 1) - 1; /* Ende der Zeile to, ohne deren '\n' */
|
||||
const ein = r.lines.join('\n');
|
||||
/* Schreibmarke: dieselbe Spalte in derselben Zeile des Blocks, der um `shift`
|
||||
Zeilen gewandert ist. Zeilen, die nicht zum eigenen Block gehörten (Cursor
|
||||
in einem ID-Block hinter ---), liegen außerhalb des Bereichs und behalten
|
||||
ihren Offset — der Text ist gleich lang geblieben. */
|
||||
const newLine = (line - 1 >= r.from && line - 1 <= r.to && istImEigenenBlock)
|
||||
? line + r.shift : line;
|
||||
keyboardOnJump(true); /* Telefon: Fokus ohne Tastatur (D25) */
|
||||
src.focus({preventScroll: true});
|
||||
const top = src.scrollTop;
|
||||
writeAt(von, bis, ein, 0, 0); /* Auswahl setzen wir gleich selbst */
|
||||
const pos = Math.min(offsetOfLine(src.value, newLine - 1) + col,
|
||||
offsetOfLine(src.value, newLine) - 1);
|
||||
src.setSelectionRange(pos, pos);
|
||||
src.scrollTop = top;
|
||||
scrollEditorToOffset(pos); /* nur, wenn die Zeile aus dem Bild gerückt ist */
|
||||
const btn = dir < 0 ? moveUpBtn : moveDownBtn;
|
||||
if(btn) flashBtn(btn);
|
||||
}
|
||||
```
|
||||
|
||||
Dabei ist `offsetOfLine(text, idx)` eine kleine Hilfsfunktion: Offset des
|
||||
Anfangs der 0-basierten Rohzeile `idx` (`idx === Zeilenzahl` ⇒
|
||||
`text.length + 1`, damit `bis` für die letzte Zeile stimmt). Ob es sie schon
|
||||
gibt, vorher mit `grep -n "function offsetOf\|lineStart" frontend/src/app.js`
|
||||
prüfen; sonst anlegen.
|
||||
|
||||
`istImEigenenBlock` ist wahr, wenn die Cursor-Zeile (0-basiert `line-1`) im
|
||||
Bereich `[N, endN]` des **eigenen** Blocks lag — `moveNodeBlock` sollte dafür
|
||||
`from`/`to` des eigenen Blocks mitliefern (im Rückgabeobjekt z. B. als
|
||||
`own: [N, endN]`); die Signatur in §4.1 entsprechend ergänzen. Stand die
|
||||
Schreibmarke im Nachbar-Block (kann sie nicht — die Cursor-Zeile bestimmt
|
||||
den eigenen Block), braucht es keinen Fall.
|
||||
|
||||
Warum **`writeAt` und nicht `replaceTextUndoable`**: Letztere sucht den
|
||||
kleinsten abweichenden Bereich und klemmt eine Schreibmarke **innerhalb**
|
||||
des Bereichs an dessen Anfang — für einen Block, der gerade darin wandert,
|
||||
wäre das die falsche Stelle. Hier ist der Bereich bekannt und die neue
|
||||
Position berechenbar. `writeAt` feuert `input`, daran hängen `render()`,
|
||||
`saveSrc()` und der Live-Push (D76) von selbst; **kein** zusätzlicher
|
||||
Aufruf nötig. Der Rückfall in `writeAt` (`src.value = …`, wenn `execCommand`
|
||||
scheitert) ist Absicht (D14: der richtige Zustand geht vor der Historie).
|
||||
|
||||
`flashBtn` (~Zeile 3435) gibt den kurzen Petrol-Blitz mit Haken — dafür
|
||||
braucht der Knopf die beiden SVGs `ic-main`/`ic-done` wie `saveBtn`; **oder**
|
||||
der Blitz entfällt, weil der Text sich sichtbar bewegt. **Empfehlung:
|
||||
weglassen.** Die Bewegung des Textes ist Rückmeldung genug (wie beim Falten,
|
||||
D38-Nachtrag 2: „die neue Farbe des Knotens ist die Rückmeldung“). Dann auch
|
||||
die `flashBtn`-Zeile oben streichen und nur ein SVG je Knopf.
|
||||
|
||||
### 5.4 Knopfzustand
|
||||
|
||||
Der Zustand hängt an der Cursor-Zeile und am Text. In `syncCaret()` (~Zeile
|
||||
2819) am Ende ergänzen:
|
||||
|
||||
```js
|
||||
updateMoveButtons();
|
||||
```
|
||||
|
||||
und
|
||||
|
||||
```js
|
||||
function updateMoveButtons(){
|
||||
const up = document.getElementById('moveUpBtn'), dn = document.getElementById('moveDownBtn');
|
||||
if(!up || !dn) return;
|
||||
const tgt = viewOnly ? {up:false, down:false} : moveTargets(src.value, caretLineOf());
|
||||
up.disabled = !tgt.up;
|
||||
dn.disabled = !tgt.down;
|
||||
}
|
||||
```
|
||||
|
||||
Zusätzlich aufrufen in `loadActiveIntoEditor()` (Dokumentwechsel) und dort,
|
||||
wo `viewOnly` gesetzt wird (`setViewOnly` o. ä., app.js ~Zeile 144).
|
||||
`moveTargets` parst den Text; bei jedem `keyup` ist das für den
|
||||
mitgelieferten Plan (≈ 1000 Zeilen) im einstelligen Millisekundenbereich —
|
||||
`render()` parst bei jedem `input` ohnehin. Wird es messbar, den letzten
|
||||
`parse()`-Baum aus `render()` wiederverwenden statt neu zu parsen; zuerst
|
||||
die einfache Fassung.
|
||||
|
||||
Die Klicks:
|
||||
|
||||
```js
|
||||
document.getElementById('moveUpBtn') ?.addEventListener('click', () => moveCaretNode(-1));
|
||||
document.getElementById('moveDownBtn')?.addEventListener('click', () => moveCaretNode(1));
|
||||
```
|
||||
|
||||
Der Klick nimmt dem Textfeld den Fokus — `moveCaretNode` holt ihn zurück
|
||||
(`src.focus`, §5.3), damit `execCommand` greift und die Schreibmarke danach
|
||||
sichtbar im Text steht. Auf dem Telefon hält `keyboardOnJump(true)` die
|
||||
Bildschirmtastatur dabei unten.
|
||||
|
||||
### 5.5 i18n (Deutsch als Quelle; die übrigen sieben Sprachen übersetzt der Umsetzende)
|
||||
|
||||
| Schlüssel | de | en |
|
||||
|-------------------|------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------|
|
||||
| `moveUpTooltip` | Knoten nach oben – vor den vorherigen Geschwisterknoten, samt Teilbaum (Alt+↑) | Move node up – before its previous sibling, subtree included (Alt+↑) |
|
||||
| `moveDownTooltip` | Knoten nach unten – hinter den nächsten Geschwisterknoten, samt Teilbaum (Alt+↓) | Move node down – after its next sibling, subtree included (Alt+↓) |
|
||||
| `hint_move` | Alt+↑/↓ verschiebt den Knoten der Cursor-Zeile unter seinen Geschwistern – mit Beschreibung und Teilbaum. | Alt+↑/↓ moves the node of the caret line among its siblings – description and subtree included. |
|
||||
|
||||
Die Legenden-Zeile einhängen: in der Legenden-Funktion (~Zeile 4787)
|
||||
`…${esc(t('hint_save'))}<br>${esc(t('hint_move'))}</div>`. Alle neun
|
||||
Sprachen (`de en es fr pl ru hi zh ja`) bekommen alle drei Schlüssel — ein
|
||||
Loch in der Tabelle wird beim nächsten Durchsehen für einen Fehler gehalten
|
||||
(D58-Nachtrag). Nach dem Eintragen die Syntaxprobe laufen lassen:
|
||||
`cd frontend && npx esbuild src/app.js --outfile=/dev/null` — `npm test`
|
||||
importiert `app.js` nicht (D91-Nachtrag 8).
|
||||
|
||||
### 5.6 Verhalten in besonderen Lagen
|
||||
|
||||
- **Geteiltes Dokument (`?live=`)**: nichts Besonderes. Der Umbau ist eine
|
||||
gewöhnliche Textänderung; nach 0,6 s Ruhe geht sie als Zeilen-Diff an den
|
||||
Server (D76/D79). Der Diff ist größer als beim Tippen (zwei Blöcke), der
|
||||
Server rebased ihn wie jeden anderen; bei echter Überlappung mit fremden
|
||||
Änderungen an denselben Zeilen kommt das Konflikt-Band. Kein Sonderfall.
|
||||
- **Nur-Ansehen (D94 §6.5)**: Knöpfe deaktiviert, Taste wirkungslos
|
||||
(`viewOnly`-Wächter in `moveCaretNode` **und** `updateMoveButtons`).
|
||||
- **Faltung**: Marken wandern mit ihrer Zeile; die Sitzungs-Überlagerungen
|
||||
(`foldOverrides`) hängen am Label-Pfad (D38) und bleiben gültig. Nichts zu
|
||||
tun.
|
||||
- **Cursor in einem eingeklappten Teilbaum**: Das Verschieben arbeitet auf
|
||||
dem Text, nicht auf dem DOM — der eingeklappte Knoten wird wie jeder
|
||||
andere verschoben. Der Neubau zeichnet den Zustand.
|
||||
- **Cursor in einem ID-Block hinter `---`**: Der beschriebene Knoten wird
|
||||
verschoben (§4.2); die Schreibmarke bleibt an Ort und Stelle (Offsets hinter
|
||||
dem Bereich ändern sich nicht).
|
||||
- **Cursor auf Leer-/Kommentarzeile / `---` / ohne Knoten**: Knöpfe
|
||||
deaktiviert, Taste tut nichts. Dieselbe stille Regel wie beim Alt+Klick
|
||||
(D25).
|
||||
|
||||
## 6. Tests (`frontend/tests/move.test.js`, neu)
|
||||
|
||||
Vitest, Muster wie `tests/fold.test.js`. Mindestens diese Fälle; jeder als
|
||||
Text → erwarteter Text, zeichengenau (`toBe`, nicht nur „enthält“):
|
||||
|
||||
1. **Einfacher Tausch nach oben/unten** zweier Blatt-Geschwister:
|
||||
`A\n - B\n - C` → Cursor Zeile 3, `dir -1` ⇒ `A\n - C\n - B`;
|
||||
`shift === -1`; nach unten von Zeile 2 ⇒ dasselbe Ergebnis, `shift === 1`.
|
||||
2. **Teilbaum wandert mit**: `A\n - B\n - B1\n - B2\n - C` → C
|
||||
nach oben ⇒ `A\n - C\n - B\n - B1\n - B2`.
|
||||
3. **`"`-Zeilen wandern mit**: `A\n - B\n " Beschreibung B\n - C` →
|
||||
B nach unten ⇒ `A\n - C\n - B\n " Beschreibung B`.
|
||||
4. **`\`-Fortsetzungen wandern mit** (auch wenn die Fortsetzung anders
|
||||
eingerückt ist): `A\n - B lang \\\n weiter (L)\n - C` → C hoch ⇒
|
||||
`A\n - C\n - B lang \\\n weiter (L)`.
|
||||
5. **Cursor in der Fortsetzung / in der `"`-Zeile** wählt den Knoten:
|
||||
Cursor Zeile 3 in Fall 4 (die Fortsetzung) und `dir +1` ⇒ B wandert
|
||||
hinter C; Rückgabe `shift === 1`, und `own` deckt Zeile 2–3 ab.
|
||||
6. **Abstand bleibt zwischen den Blöcken**: `A\n - B\n\n - C` → C hoch ⇒
|
||||
`A\n - C\n\n - B`. Kommentar-only-Zeile im Abstand ebenso.
|
||||
7. **Kommentar-/Leerzeile innerhalb des Blocks wandert mit**:
|
||||
`A\n - B\n - B1\n %% Notiz\n - B2\n - C` → C hoch ⇒ Notiz
|
||||
steht weiter zwischen B1 und B2.
|
||||
8. **Kein Geschwister**: einziges Kind, erstes Kind nach oben, letztes Kind
|
||||
nach unten ⇒ `null`; `moveTargets` liefert entsprechend
|
||||
`{up:false, down:true}` usw.
|
||||
9. **Nicht über den Elternknoten hinaus**: `A\n - B\n - B1\n - C` →
|
||||
B1 nach unten ⇒ `null` (C ist kein Geschwister von B1).
|
||||
10. **Wurzelknoten** sind untereinander Geschwister: `A\n - A1\nB\n - B1`
|
||||
→ B hoch ⇒ `B\n - B1\nA\n - A1`.
|
||||
11. **Beschreibungsteil bleibt stehen**: `A\n - #b: B\n - #c: C\n---\n#b\n Text b\n#c\n Text c`
|
||||
→ C hoch ⇒ Baumteil getauscht, alles ab `---` byte-identisch; Cursor
|
||||
auf Zeile 8 (` Text c`) verschiebt C ebenfalls (Auflösung über
|
||||
`descLines`); Cursor auf Zeile 4 (`---`) ⇒ `null`.
|
||||
12. **Faltmarke wandert mit**: `A\n - [ ] > B\n - B1\n - C` → C hoch ⇒
|
||||
`A\n - C\n - [ ] > B\n - B1`.
|
||||
13. **Tab-Einrückung** zählt als zwei Leerzeichen: `A\n\t- B\n - C` → C hoch
|
||||
⇒ `A\n - C\n\t- B` (B und C sind Geschwister).
|
||||
14. **Invariante**: für jeden Fall `neu.length === alt.length` und die
|
||||
sortierten Zeilen beider Texte sind gleich (nichts geht verloren, nichts
|
||||
kommt dazu).
|
||||
15. **Idempotenz-Probe**: hoch und dann runter ergibt den Ausgangstext.
|
||||
|
||||
Gegenprobe per Mutation (Hausregel D54-Nachtrag 3): Entfernt man in
|
||||
`end()` das Überspringen der `desc`-Zeilen, müssen **genau** Fall 3 und 5
|
||||
fallen; entfernt man die Tab-Umrechnung, genau Fall 13; entfernt man das
|
||||
Zurückziehen nachlaufender Leerzeilen, genau Fall 6. Das Ergebnis der
|
||||
Gegenprobe gehört in den Commit-Text.
|
||||
|
||||
## 7. Browser-Nachmessung (vor dem Merge)
|
||||
|
||||
Dev-Server `cd frontend && npm run dev`, Werkbaum-Plan öffnen:
|
||||
|
||||
1. Cursor auf `#ed.render.vert`, Alt+↑ ⇒ die Zeile steht über
|
||||
`#ed.render.horiz`, Schreibmarke in derselben Spalte derselben Zeile,
|
||||
Diagramm zeigt die neue Reihenfolge, **ein** Strg+Z stellt alles her
|
||||
(Text und Bild).
|
||||
2. Cursor auf `#ed.fold` (hat Kinder und Faltmarken im Plan), Knopf ▼ ⇒
|
||||
der ganze Block samt Kindern steht hinter `#ed.lens`; Beschreibungsteil
|
||||
unverändert (per `git diff` der Textarea-Fassung oder Zeilenzahl
|
||||
prüfen: Zeilenzahl gleich).
|
||||
3. Cursor in eine `\`-Fortsetzung oder `"`-Zeile setzen ⇒ Knöpfe aktiv,
|
||||
Verschieben nimmt die Zeile mit.
|
||||
4. Cursor auf eine Kommentarzeile oder in `---` ⇒ beide Knöpfe deaktiviert.
|
||||
5. Erstes Kind: ▲ deaktiviert, ▼ aktiv; letztes Kind umgekehrt.
|
||||
6. Nur-Ansehen (zweites Fenster auf demselben lokalen Dokument, D94) ⇒
|
||||
beide deaktiviert, Alt+↑ tut nichts.
|
||||
7. 375 px (`body.mobile`): Editor-Titelzeile bleibt einreihig; Tipp auf ▲
|
||||
verschiebt, ohne dass die Bildschirmtastatur hochkommt (soweit in der
|
||||
Emulation prüfbar — die Tastatur selbst ist Handtest, D17-Nachtrag 4).
|
||||
8. Geteiltes Dokument gegen ein lokales Backend: Verschieben kommt beim
|
||||
zweiten Client an (Feed) — sofern ein Backend läuft; sonst als
|
||||
Handtest notieren.
|
||||
|
||||
Werkzeuggrenzen, die dabei zuschnappen können (alle schon bezahlt):
|
||||
`execCommand` braucht Fensterfokus (D57, D91-Nachtrag 8) — in einer
|
||||
verborgenen Automations-Fläche greift der `src.value`-Rückfall; synthetische
|
||||
Tastendrücke kommen ggf. mit `e.key === ''` an (D67); `document.hidden`
|
||||
drosselt Timer (D79).
|
||||
|
||||
## 8. Alternativen (verworfen)
|
||||
|
||||
- **Neue Editor-Komponente (CodeMirror 6)**: bringt „Zeile verschieben“
|
||||
mit, kostet 120 kB gzip und berührt ein Dutzend Entscheidungen (D76 misst
|
||||
das). Für ein Feature, das eine Text→Text-Funktion und zwei Knöpfe ist,
|
||||
außer Verhältnis. Bleibt eine eigene Frage.
|
||||
- **Verschieben per Drag & Drop im Diagramm**: die stärkere Geste, aber
|
||||
ein anderes Feature (Umhängen zwischen Eltern eingeschlossen, Touch-
|
||||
Konflikt mit langem Druck D25 und Scrollen). Nicht hier.
|
||||
- **Nur die Cursor-Zeile verschieben** (wie ein Texteditor): zerreißt in
|
||||
dieser Notation Teilbäume und Beschreibungen — der Nutzerwunsch nennt
|
||||
ausdrücklich `"`-Zeilen und `\`-Fortsetzungen. Der Block ist die Einheit.
|
||||
- **Abstand-Leerzeilen mitnehmen**: dann sammelten sich Leerzeilen an einem
|
||||
Ende der Gruppe; als Abstand zwischen den Blöcken bleiben sie neutral.
|
||||
- **`e.repeat` filtern**: ein gehaltener Pfeil soll weiterschieben — das
|
||||
Vorbild der IDEs; jeder Schritt bleibt ein eigener Undo-Schritt.
|
||||
|
||||
## 9. Nachziehen der Dokumente (im selben Zug, nicht am Ende)
|
||||
|
||||
### 9.1 SPEC §9 — neuer Abschnitt, hinter „Zeilennummern im Texteditor“
|
||||
|
||||
```
|
||||
### Knoten verschieben (Editor-Titelzeile)
|
||||
Zwei Knöpfe (▲ / ▼) in der Titelzeile des Text-Editors — Tastatur **Alt+↑**
|
||||
/ **Alt+↓** im Textfeld — verschieben den Knoten der **Cursor-Zeile** vor
|
||||
seinen vorherigen bzw. hinter seinen nächsten **Geschwisterknoten**.
|
||||
Verschoben wird der ganze **Block**: die Zeile, ihre Fortsetzungen (`\`,
|
||||
§1), ihre Beschreibungszeilen (`"`, §1) und der Teilbaum darunter, samt
|
||||
Kommentar- und Leerzeilen darin; Leerzeilen **zwischen** zwei Blöcken bleiben
|
||||
zwischen ihnen stehen. Steht der Cursor in einer Fortsetzungs- oder
|
||||
Beschreibungszeile oder in einem ID-Block hinter `---`, gilt der beschriebene
|
||||
Knoten (§9, Sprung). Der Beschreibungsteil hinter `---` wird nie angefasst —
|
||||
seine Blöcke sind per ID adressiert. Über die Geschwistergruppe hinaus wird
|
||||
nicht verschoben; ohne Geschwister in der Richtung ist der Knopf deaktiviert.
|
||||
Die Änderung ist eine gewöhnliche, **undo-fähige** Textänderung (ein
|
||||
Schritt) — in einem geteilten Dokument (`?live=`) sehen sie damit alle. Im
|
||||
Nur-Ansehen-Modus (D94) ist sie gesperrt. Eine Eingabehilfe, keine Notation:
|
||||
Der Parser sieht nie etwas davon, `llms.md` (§13) bleibt unberührt. Siehe D95.
|
||||
```
|
||||
|
||||
### 9.2 CHANGELOG (`## 2026-09-…`, oberster Tag)
|
||||
|
||||
```
|
||||
- Two buttons in the editor title bar — and Alt+↑/↓ in the text — move the node of the caret line before its previous or after its next sibling, taking its continuation lines, `"` descriptions and whole subtree along in one undo step
|
||||
```
|
||||
|
||||
### 9.3 Legende
|
||||
|
||||
`hint_move` als dritte Zeile der Bedienungs-Zeile (§5.5).
|
||||
|
||||
### 9.4 `frontend/CLAUDE.md`
|
||||
|
||||
Falls dort eine Liste der Text→Text-Rückschreiber steht (`setFoldMark`,
|
||||
`expandShortIds`, `setStatusBox`), `moveNodeBlock` ergänzen. Sonst nichts.
|
||||
|
||||
### 9.5 DECISIONS — D95 anhängen (Entwurf; beim Bauen um Messwerte ergänzen)
|
||||
|
||||
```
|
||||
## D95 — Knoten im Text verschieben: Block statt Zeile, Alt+↑/↓, kein neuer Editor
|
||||
Nutzerwunsch: Knöpfe in der Editor-Titelzeile, die den Knoten der
|
||||
Cursor-Zeile vor den vorherigen bzw. hinter den nächsten Geschwisterknoten
|
||||
schieben, samt `"`-Zeilen und `\`-Fortsetzungen. Gebaut als reine
|
||||
Text→Text-Regel (`moveNodeBlock` in parser.js, neben `setFoldMark`,
|
||||
`expandShortIds`, `setStatusBox`), geschrieben über `writeAt` als ein
|
||||
Undo-Schritt; kein CodeMirror (D76 hält das als eigene Frage offen).
|
||||
Verschoben wird der **Block** — Zeile, Fortsetzungen, Beschreibungen,
|
||||
Teilbaum —, nicht die Zeile: In dieser Notation ist die Zeile keine
|
||||
Einheit, der Knoten ist es. Leerzeilen zwischen Blöcken bleiben Abstand.
|
||||
Nicht über die Geschwistergruppe hinaus (das wäre Umhängen, eine eigene
|
||||
Entscheidung); der Beschreibungsteil hinter `---` bleibt stehen (per ID
|
||||
adressiert). Tastatur Alt+↑/↓ wie in IntelliJ und VS Code; ohne Alt
|
||||
gehören die Pfeile der Schreibmarke und der ID-Vorschlagsliste (D63).
|
||||
`e.repeat` bewusst nicht gefiltert. Vollständige Bauanleitung, Fälle und
|
||||
Alternativen: docs/rfc/003-knoten-verschieben.md.
|
||||
```
|
||||
|
||||
### 9.6 Plan (`docs/examples/werkbaum.werkbaum`)
|
||||
|
||||
Im Baumteil unter `#ed`, nach der Zeile `#ed.depcomplete` (Zeile ≈ 88):
|
||||
|
||||
```
|
||||
- [x] #ed.move: Move a node up or down among its siblings, block and all (S) %% docs/rfc/003-knoten-verschieben.md, D95
|
||||
```
|
||||
|
||||
(`[x]` beim Mergen; `[^]` setzt erst der Deploy, D30. Bis zum Bauen: `[ ]`.)
|
||||
Im Beschreibungsteil, alphabetisch bei den `#ed.*`-Blöcken:
|
||||
|
||||
```
|
||||
#ed.move
|
||||
Two buttons in the editor title bar and Alt+Up/Down move the node of the
|
||||
caret line before its previous or after its next sibling. The whole block
|
||||
travels — continuation lines, quote descriptions, the subtree — as one
|
||||
undo step; the description part behind --- stays where it is.
|
||||
```
|
||||
|
||||
Danach `npm test` — der Plan hat Snapshot-/Warnungs-Tests; 0 Warnungen
|
||||
müssen bleiben (Größenprüfung D62: `#ed` ist XXL und warnt nie, aber die
|
||||
Elterngröße einer Zwischenebene könnte kippen — dann ehrlich nachziehen wie
|
||||
in D64/D91-Nachtrag 11).
|
||||
|
||||
## 10. Umsetzungsreihenfolge
|
||||
|
||||
1. `moveNodeBlock`/`moveTargets` in `parser.js` + `tests/move.test.js`
|
||||
(§4, §6), Gegenprobe per Mutation. `npm test` grün.
|
||||
2. Knöpfe, Tastatur, `moveCaretNode`, `updateMoveButtons`, i18n ×9,
|
||||
Legenden-Zeile (§5). `npx esbuild src/app.js --outfile=/dev/null`.
|
||||
3. Browser-Nachmessung (§7), Befunde in den Commit-Text.
|
||||
4. SPEC §9, CHANGELOG, D95, Plan-Knoten (§9) — im selben Commit oder direkt
|
||||
danach, nie später.
|
||||
|
||||
Commit-Texte auf Deutsch, Betreff nach dem Muster der Historie
|
||||
(`feat(editor): …`), Abschluss `Co-Authored-By: Claude Fable 5.1
|
||||
<noreply@anthropic.com>`. Direkt auf `main`, kein PR.
|
||||
|
||||
## 11. Revisionsgeschichte
|
||||
|
||||
- 2026-09-03 — erste Fassung aus dem Nutzerwunsch; als Bauanleitung für
|
||||
einen Coding-Agenten geschrieben. Nichts gebaut.
|
||||
Reference in New Issue
Block a user