editor
Der Text-Editor in zid: Bearbeiten/Auto-Indent/Autoclose, Maus, Mehrfach-Cursor, Word-Wrap, Suche/Ersetzen, Statusleiste, Panes, externe Änderungen, Kodierung (Windows-1252 laden/speichern) — und die
SKILL.md.claude/skills/editor/SKILL.md
GitHubUnit-Tests in code_editor.zig
Tests in src/editor/code_editor.zig laufen nur, weil die Datei eigenes Test-Root ist
(code_editor_tests in build.zig, wio-Symbol-Hack unter is_test in der Datei). Tests in
importierten Modulen führt der Runner nicht aus; zig build test --summary all zeigt die
Zähler pro Modul, ein absichtlich kaputter Test ist der schnellste Beweis.
Editor: Bearbeiten, Maus, Statusleiste, Panes
- Reine Textlogik in
src/editor/edit_ops.zig(unit-getestet): Auto-Indent (newlineInsertion: Einrückung übernehmen, nach{([eine Stufe mehr,{}-Paar aufspannen), Autoclose-Regeln (autoclosePolicy: Paar, drüberspringen, neben Wortzeichen normal;deletesPairfür Backspace), Ein-/Ausrücken, Kommentar umschalten (commentPrefixForPathje Endung, Markdown/HTML keins),languageNameForPath,wordAt,looksLikeDefinition. Der Editor wendet sie indispatchActionan (Actions IndentLines/OutdentLines/ToggleComment/MoveLineUp/Down/DuplicateLine/GotoLine/Replace/ GotoDefinition; Keymap Shift+Tab, Ctrl+/, Alt+↑/↓, Ctrl+Shift+D, Ctrl+G, Ctrl+H, F12; Tab mit mehrzeiliger Auswahl rückt ein). Zeilenoperationen laufen überreplaceLineSpan. - Ctrl+X ohne Auswahl schneidet die ganze Zeile aus (VS Code, Zed): Zeile plus Umbruch in
die Zwischenablage, dann
DeleteLine. Mit Auswahl bleibt Cut wie gehabt. - Metrik:
insert_charsaddiert die Chunk-Breite ausegc_chunk_widthzur Cursor-Spalte; die muss die echte Breite liefern, sonst steht der Cursor nach Einfügen/Autoclose falsch. - Undo-Schritte und Undo-Cursor:
snapshotForUndolegt die Cursor-Position (zeile:spalte) als Metadaten in den flow-core-Undo-Stand;afterUndoRedosetzt den Cursor dorthin (begrenzt) statt an den Dateianfang und nimmt den Geändert-Status ausBuffer.is_dirty(). Eine Tipp-Gruppe endet, wenn der Cursor nicht mehr hinter dem zuletzt getippten Zeichen steht (typing_end), und beim Speichern (markSaved): nur dann ist der root des nächsten Undo-Standslast_save, und Undo zurück dorthin macht den Tab sauber. - Cursor-Spalte nach Tippen kommt aus
insert_chars(result[1]), nie aus der Byte-Länge: ein Umlaut ist 2 Bytes, aber 1 Spalte; mit Byte-Länge landet der Cursor hinter dem Zeilenende und jede weitere Eingabe scheitert still (INSERT FAILED). - Anzeige im Editor (
renderRowOverlays, schwebende Elemente über dem Zeilentext, x = Spalte ×charWidth): Einrück-Guides je 4 Spalten führenden Whitespace, Whitespace-Punkte/Tab-Striche (show_whitespace, Standard aus), Klammerpaar am Cursor (findBracketPair, max. 2000 Zeilen, pro Frame; Rahmen um beide Klammern; RPCeditor_state.bracket_pair). Minimap rechts (renderMinimap, 2 px je Zeile, Fenster um den Viewport, Klick springt) und horizontale Scrollbar unten (renderHScrollbar, nur wenn eine sichtbare Zeile breiter als der Ausschnitt ist). View → Toggle Minimap / Render Whitespace / Indent Guides (gemerkt inuser_state). - Kodierung: Dateien ohne gültiges UTF-8 lädt flow-core als Windows-1252
(
unicode.cp1252_decode, FlagBuffer.file_utf8_sanitized) und speichert sie wieder so (store_to_file_const→cp1252_encode). Jedes Byte ist ein Zeichen, unveränderte Bytes bleiben exakt. Ein Zeichen außerhalb von 1252 lässt das Speichern miterror.NotInWindows1252scheitern; Datei unverändert, Meldung im Editor, Tab-Schließen bleibt offen, Autosave wartet auf die nächste Änderung (save_failed_edit_ms). Der Watcher vergleicht überBuffer.matches_file_bytesin der Kodierung der Datei, sonst lüde jeder eigene Save neu und verwürfe Undo; er zählt auch den zuletzt gespeicherten Stand als eigen, weil sein Ereignis verspätet kommt, wenn schon weitergetippt wurde. Statusleiste „Windows 1252“ wie VS Code. Umwandeln nach UTF-8 nur bewusst: Command Palette „Save with Encoding: UTF-8“ (save_as_utf8,UI.saveAsUtf8). Alle anderen Schreibwege halten die Kodierung der Datei ebenso: Ersetzen im Projekt (replaceInFilekodiert den Ersatz, Trefferliste dekodiert zur Anzeige) und die Agent-Werkzeuge (decodeFile/encodeForinagent_actions.zig). E2Epython3 scripts/e2e_encoding.py. - Backup vor dem Speichern löst relative Pfade per
realpathAllocauf, weilaccessAbsolutebei relativen Pfaden inunreachableläuft. - Leerzeichen am Zeilenende:
CodeEditor.saveentfernt standardmässig Leerzeichen und Tabs vor dem Schreiben als eigenen Undo-Schritt; Bytebereiche werden direkt im Rope gelöscht, Kodierung und Zeilenenden bleiben erhalten. File → Toggle Trim Trailing Whitespace schaltet die Option für alle Panes um (user_state.trim_trailing_whitespace, beim Split übernommen). Autosave, Tab-Schliessen und Save All beim Beenden verwenden denselben Speicherweg;UI.saveBuffernimmt für einen nicht sichtbaren Buffer einen temporären Editor. E2E:scripts/e2e_save_whitespace.py(CRLF/Windows-1252, Undo, aus/an, Neustart, Autosave, neuer Pane, nicht sichtbarer Tab bei Save All). Ein Schreibfehler lässt die Bereinigung als ungespeicherte Änderung samt Undo-Verlauf bestehen. - Suchleiste (
CodeEditor.find=find_bar.FindState, Leistefind_bar.render, gemeinsam mit der Markdown-Vorschau; Logik insrc/editor/find_ops.zig): inkrementell beim Tippen, Enter/Shift+Enter weiter/zurück mit Umbruch, Escape schließt, markierter Text wird Suchbegriff. Ctrl+F bei offener Leiste markiert den Begriff neu. Die Widget-IDs (find_widget,find_input,replace_*,goto_*) tragen das Editor-Salz (idi), sonst melden zwei Panes mit offener Leiste duplicate_id. Breitzeichen zählen als eine Spalte. E2E:python3 scripts/e2e_find_preview.py(Vorschau, Editor-Tab, Split). - Suchleiste: Ctrl+H zeigt die Ersetzen-Zeile, Tab wechselt das Feld, Enter ersetzt den Treffer,
Alt+Enter alle (
replaceAll, ein Undo-Schritt). Ctrl+G öffnet „Go to line“ (nur Ziffern). - Suche: Optionen Alt+C (Groß/Klein), Alt+W (Ganzwort), Alt+R (Regex) in der Suchleiste, Badges
„Aa W .*“. Regex-Engine
src/editor/tiny_regex.zig(Backtracking: Literale,.,* + ?, Klassen,^ $,\d \w \s \b, Gruppen,|; keine Rückverweise/Captures, keine lazy Quantoren; unit-getestet).find_ops.findOptsrechnet Spalten als Anzeigespalten (Tab = 4) und nimmt das Trefferende aus dem Treffer, nicht aus der Musterlänge. Der erste Tastendruck nach Ctrl+F ersetzt den alten Begriff (replace_on_type). Watcher: identische Ereignisse werden nur innerhalb von 100 ms zusammengefasst, damit eine zweite externe Änderung derselben Datei nicht verloren geht. - Maus: Dreifachklick markiert die Zeile (
click_count), Shift+Klick erweitert vom Anker, Ctrl+Klick/ F12 springen zur ersten Definitionszeile des Worts im selben Buffer (kein LSP: der Client insrc/lspwird nirgends gestartet), Ziehen über den Rand scrollt (autoScrollWhileDraggingim Render). - Statusleiste (oben, neben dem Branch):
UI.statusText— Ln/Col, Auswahl, LF/CRLF, Kodierung, Sprache, „Spaces: 4“; RPCui_state.status_text. - Datei außerhalb geändert: Watcher-
file_changedoderfile_created(atomares Ersetzen per rename meldet IN_MOVED_TO) →UI.handleExternalChange: gleicher Inhalt (eigener Save) ignoriert, ungeänderter Buffer wird still neu geladen, geänderter fragt („File Changed“: Reload / Keep Mine). Symlink-Ordner: der Linux-Watcher steigt auch in Link-Ordner ab (inotify folgt dem Link; schon bekannter Watch-Deskriptor = kein zweiter Abstieg), undbufferKeyForPathfindet den Buffer notfalls über realpath, weil Ereignis- und Öffnungspfad verschiedene Schreibweisen derselben Datei sein können. E2E:python3 scripts/e2e_external_change.py(in-place, atomic, Symlink im und außerhalb des Projekts). Unter Windows nimmt die Suite ohne Symlink-Recht Junctions und schreibt LF (newline="\n"; im Textmodus käme CRLF und schon der Ausgangsvergleich schlüge fehl). - Panes: Ctrl+\ splittet, Ctrl+Alt+Pfeil oder Chord Ctrl+K dann Pfeil wechselt geometrisch
(
focusPaneüber die Pane-Bounds des letzten Frames), Ctrl+Shift+E fokussiert den Explorer, Ctrl+J wechselt zum Terminal-Tab und zurück (terminal_return_index). Ctrl+K erreicht die Shell im Terminal nicht. - RPCs:
key_press_alt(name, ctrl, shift, alt),click_mods(x, y, ctrl, shift),editor_state.selection,ui_state.active_pane_index.python3 scripts/e2e_editor.pydeckt alles ab. - Mehrfach-Cursor: Ctrl+D markiert das Wort unter dem Cursor, jedes weitere Ctrl+D fügt das
nächste Vorkommen als
ExtraCursor(Cursor + Anker) hinzu (selectNextOccurrence); Ctrl+Alt+↑/↓ setzt Cursor in der Nachbarzeile (addCursorVertical).dispatchAction/handleCharsind Wrapper: mit Extra-Cursorn läuftforEachCursorüber alle Cursor von unten nach oben (dispatchSingle/handleCharSingleje Cursor,in_multiverhindert Snapshots pro Cursor, ein Undo-Schritt für alle). Schon bearbeitete Cursor derselben Zeile werden um die Breitenänderung verschoben, tiefere um die Zeilenänderung; sonst zeigteprevCharBoundaryhinter das Zeilenende. Escape/Klick/Enter lösen auf (multiSafelistet, was mit mehreren Cursorn erlaubt ist). RPCeditor_state.cursors. - Word-Wrap (Alt+Z, View → Toggle Word Wrap, gemerkt in
user_state): Soft-Wrap insrc/editor/wrap_ops.zig(unit-getestet): Segmente vonwrapCols()Anzeigespalten (sichtbare Spalten minus Minimap minus 1; Codepoint = 1, Tab = 4), Bruch hinter dem letzten Whitespace, sonst hart. Der Renderer zeichnet je Buffer-Zeile ein Segment pro Reihe (renderLinebekommt Segment, Startspalte undis_last; Cursor/Auswahl/Overlays werden auf das Segment geklemmt), Fortsetzungsreihen ohne Zeilennummer und mit eigenen Clay-IDs (roww/gutterw/codewje sichtbarer Reihe; das erste Segment behältrow/gutter/codeje Buffer-Zeile fürelement_bounds_i). Maus:hitRowfindet Zeile + Segmentanfang,colFromXmisst ab dort.ensureCursorVisiblesetztview.col = 0,view.colsriesig und rücktview.rowvor, bis die Reihen bis zum Cursor passen. Bewusst einfach: Cursor ↑/↓ und Scrollen arbeiten in Buffer-Zeilen, nicht in Reihen (VS Code bewegt sich reihenweise); horizontale Scrollbar und Shift+Mausrad sind aus. Die Obergrenze fürs Scrollen istmaxViewRow: sie summiert bei Wrap die Reihen von hinten, bis der Schirm voll ist; Zeilen minus sichtbare Zeilen ließe das Dateiende unerreichbar. Rad, Leiste (Daumen übertotalVisualRows) und Ziehen benutzen sie. RPCeditor_state.word_wrap,editor_state.visual_rows(Reihen der Cursor-Zeile). - Neue Editoren erben Optionen:
splitActivePanekopiert Minimap/Whitespace/Guides/Wrap, Schriftgröße und Theme vom Ausgangs-Editor (copyEditorOptions), weilloadUserStatenur die beim Start vorhandenen Leaves erreicht. - Clay
getElementDatavergisst nichts: IDs, die nicht mehr gerendert werden, bleibenfoundmit alter Geometrie. E2E-Prüfungen auf „Element ist weg“ sind wertlos; Zustand per RPC prüfen. Für ausgeblendete Kontextmenü-Einträge geht es trotzdem: der Eintrag muss innerhalb des frisch gezeichneten<prefix>_containerliegen (menu_entry_visibleinscripts/e2e_marp_pdf.py). - E2E nie mit der echten Konfiguration:
start_zidinscripts/e2e_open_folder.pysetzt ohne eigenesenvfrischeXDG_CONFIG_HOME/XDG_DATA_HOMEuntertmp/e2e_env/<log-name>. Mit~/.config/zid/state(Word-Wrap, Schriftgröße, Sidebar-Breite) messen Suiten Fremdzustand und überschreiben ihn. Neue Suiten starten zid deshalb überstart_zid, nie perPopen. - Split behält Chat und Terminal:
splitActivePanegibt die bisherige Tab-Leiste an die erste Hälfte weiter, nur die neue Hälfte bekommtcloneFrom(ohne Chat und Terminal), sonst verschwinden Chat-Tabs und Terminal-Instanzen. E2E:e2e_tabs.py. - setText verwirft den Undo-Verlauf.
libs/flow-coregibt inBuffer.loaddie Leaf-Puffer des vorherigen Ladevorgangs frei (Leak-Fix gegenüber upstream flow), auf deren Bäume alle Undo-/Redo-Knoten zeigen (sonst „switch on corrupt value“ inwalk_from_line_begin_const_internal).CodeEditor.setTextsetzt deshalbundo_head/redo_headauf null, beendet die Tipp-Gruppe und löscht Extra-Cursor; Undo über einen Reload hinweg gibt es bewusst nicht.
Textfelder: zwei Sorten, klar getrennt
- Mehrzeilig →
CodeEditor: Haupteditor, KI-Chat-Eingabe und das Commit-Feld der Source-Control-Ansicht. Damit gibt es dort Umbruch, Rückgängig, Mausauswahl, Kontextmenü und unbegrenzte Länge. - Einzeilig →
line_edit+explorer_ops.EditBuffer: Umbenennen und Filter im Explorer, Schnellöffner, Ordner-Dialog. Klein gehalten, kein Umbruch (wrap_mode = .none). Rückgängig gibt es dort nur einen Schritt (undoEdit/redoEdit): zusammenhängendes Tippen ist eine Gruppe, eine Cursorbewegung schliesst sie. Doppelklick markiert das Wort (selectWordAtCursor); die Zeit dafür kommt ausstd.time.milliTimestamp, damit die Aufrufer keine Uhr durchreichen müssen. line_editin einem Dialog brauchtConfig.z_indexüber dem Dialog. Cursorstrich und Markierung sind schwebende Elemente, und Clay sortiert z-Indizes global, nicht relativ zum Elternteil. Mit dem Standard 10 liegen sie unter Ordner-Dialog und Picker (z 2000); beide Felder stehen deshalb auf 2002. E2E prüft das Pixel am Cursor ine2e_open_folder.py.line_editscrollt waagrecht:EditBuffer.scroll_xfolgt dem Cursor (explorer_ops.followCaret, unit-getestet), das Textelement ist selbst ein Clip-Container mitchild_offset, Markierung und Strich ziehen den Versatz selbst ab (Clay versetzt schwebende Kinder nicht) und hängen mitclip_to = .to_attached_parentam Feld; Klick und Ziehen rechnenscroll_xein. Der Rahmen um das Feld darf kein.cliphaben: Clay ziehtgrow-Kinder eines Clip-Elternteils auf Inhaltsbreite, das Feld wuchs dann mit dem Text mit. E2E: Schritt 2b ine2e_open_folder.py.- Jeder eingebettete
CodeEditorbraucht die Modifier:UI.setCtrlState/setAltState/setShiftStatereichen sie an Chat und Commit-Feld weiter. Ohne das greift die Keymap des Editors nicht und Ctrl+Z tut nichts. - Ebenso Uhr und Zwischenablage:
time_msbekommt inUI.updateder aktive Editor, der Chat (updateTimeMs) und das Commit-Feld; ohne Uhr gilt jeder zweite Klick in dieselbe Zeile als Doppelklick. Copy/Cut/Paste laufen überclipboard_hook(gesetzt inensureEditorHooksfür Panes, Chat und Commit-Feld) →UI.setClipboard, damit auch headless etwas ankommt. Neuer eingebetteter Editor: beides mitverdrahten. - Die Auswahl eines
CodeEditorhat kein eigenes Clay-Element (anders alsline_edit,<feld>_sel). E2E prüfen den Text:scm_state.changes.selected_text,editor_state.selection.