Text Editor Architecture (Current Implementation + CodeEditor Roadmap)

This document describes the current text editor architecture in XenoAtom.Terminal.UI and the next steps required to evolve it into a production-grade CodeEditor.

The goals are:

  • Make the current behavior explicit (so we can test/maintain it).
  • Preserve API compatibility for TextBox / TextArea while enabling future growth.
  • Provide a concrete roadmap (performance, extensibility, usability).

Current implementation: code map

Controls

  • TextEditorBase (src/XenoAtom.Terminal.UI/Controls/TextEditorBase.cs)
  • TextEditorCore (src/XenoAtom.Terminal.UI/Controls/TextEditorCore.cs) (internal)
  • TextBox (src/XenoAtom.Terminal.UI/Controls/TextBox.cs)
  • TextArea (src/XenoAtom.Terminal.UI/Controls/TextArea.cs)
  • MaskedInput (src/XenoAtom.Terminal.UI/Controls/MaskedInput.cs) (inherits TextBox)

Text model

  • ITextDocument (src/XenoAtom.Terminal.UI/Text/ITextDocument.cs)
  • ITextSnapshot (src/XenoAtom.Terminal.UI/Text/ITextSnapshot.cs)
  • TextDocument (src/XenoAtom.Terminal.UI/Text/TextDocument.cs) (string-backed)
  • DynamicTextDocument (src/XenoAtom.Terminal.UI/Text/DynamicTextDocument.cs) (delegate-backed, bridges bindable Text)
  • TextSnapshot (src/XenoAtom.Terminal.UI/Text/TextSnapshot.cs)
  • TextLine (src/XenoAtom.Terminal.UI/Text/TextLine.cs)
  • TextPosition / TextRange (src/XenoAtom.Terminal.UI/Text/TextTypes.cs)
  • TextDocumentChangedEventArgs (src/XenoAtom.Terminal.UI/Text/TextDocumentChangedEventArgs.cs)

Scrolling integration

  • ScrollModel (src/XenoAtom.Terminal.UI/Scrolling/ScrollModel.cs)
  • IScrollable (src/XenoAtom.Terminal.UI/Scrolling/IScrollable.cs)
  • ScrollViewer (src/XenoAtom.Terminal.UI/Controls/ScrollViewer.cs)

Goals (re-stated with current framework constraints)

Functional scope (what exists now)

  • Shared editing engine for single-line and multi-line editors.
  • Caret + linear selection + clipboard integration.
  • Mouse selection (drag) within the viewport.
  • Multi-line soft wrapping (WordWrap).
  • Horizontal scrolling for single-line (and for multi-line when wrapping is disabled).
  • Terminal cursor integration via ICursorProvider (caret is not rendered in-buffer).

Framework alignment requirements (non-negotiable)

  • Layout uses Measure(in LayoutConstraints) / Arrange(Rectangle) and SizeHints.
  • Binding dependency tracking relies on property usage; “hidden” backing-field reads bypass tracking.
  • Rendering is done into a CellBuffer and clipped by the renderer (no “render all then clip” hacks).
  • Input is routed events (OnKeyDown, OnTextInput, OnPointer*, OnPaste).
  • Clipboard must go through Terminal.Clipboard.
  • The caret should be a terminal cursor location (not reverse-video glyphs).

Text model: contracts and current semantics

ITextDocument and versioning

ITextDocument provides:

  • CurrentSnapshot and Version
  • Insert, Remove, Replace
  • BeginUpdate() (currently does not batch change events; reserved for future batching)
  • Changed event (TextDocumentChangedEventArgs)

Current contract assumptions:

  • Version increments when text changes.
  • The editor refreshes cached data when Version changes.
  • The document stores user input as-is (no line-ending normalization).

Snapshots and line model

TextSnapshot contains:

  • the full string text
  • LineStarts[] and per-line LineBreakLength[] (so CRLF vs LF is preserved)

TextLine exposes:

  • Start, Length, LineBreakLength
  • End and EndIncludingBreak

Bridging bindable Text with DynamicTextDocument

TextEditorBase is document-first. TextBox and TextArea each define their own bindable Text property and set:

TextDocument = new DynamicTextDocument(
    getter: () => Text ?? string.Empty,
    setter: value => Text = value);

This provides a V1-friendly two-way binding story:

  • Editing operations mutate the ITextDocument.
  • DynamicTextDocument writes back into the bindable Text property.
  • External changes to Text are picked up by DynamicTextDocument by comparing _getter() with the cached string and incrementing Version when it differs.

Important note:

  • DynamicTextDocument.Replace(...) currently performs text.ToString() for the inserted span and rebuilds the entire string. This is acceptable for V1 but not for CodeEditor-scale documents.

Editor control model: TextEditorBase + TextEditorCore

TextEditorBase responsibilities

TextEditorBase is a Visual that:

  • hosts a TextEditorCore
  • owns a ScrollModel and exposes it via IScrollable
  • forwards routed input events to TextEditorCore
  • provides the terminal cursor location via ICursorProvider
  • exposes configuration as bindable properties:
    • Placeholder
    • AcceptTab
    • WordWrap
  • exposes ITextDocument TextDocument (public, swappable)

TextEditorCore responsibilities

TextEditorCore is internal and currently owns:

  • caret index (UTF-16 index)
  • linear selection (anchor + end)
  • preferred column for vertical movement
  • a cached full text string by document.Version
  • viewport mapping and viewport-only rendering
  • scrolling behavior (“ensure caret visible”)
  • keyboard/mouse command handling

Rendering hook: segment writer

Rendering goes through a TextSegmentWriter delegate. TextEditorBase supplies a WriteTextSegment(...) virtual method used by the delegate.

Current examples:

  • MaskedInput overrides WriteTextSegment to render masked glyphs (without duplicating TextBox logic).

For CodeEditor, this hook will need to grow into a layered, styled text pipeline (see section 8.4).

Cursor policy

Caret rendering is out-of-band:

  • TextEditorCore.TryGetCursorCell(...) computes (x,y) in terminal cells.
  • The terminal cursor is moved by the host (TerminalApp) based on the focused ICursorProvider.

Layout and scrolling behavior

Internal scroll model (ScrollModel)

The editor sets:

  • viewport size from the arranged editor rectangle
  • extent based on text + wrap mode
  • offsets updated by caret movement, wheel scrolling, or scrollbars

ScrollModel.ScrollToMakeVisible(...) is used to keep the caret visible.

TextBox overflow indicators (horizontal)

TextBox uses ScrollModel.OffsetX and supports optional overflow indicators via style:

  • left indicator when Scroll.OffsetX > 0
  • right indicator when Scroll.OffsetX + ViewportWidth < ExtentWidth

When indicators are visible, TextBox shrinks the editor rectangle to reserve one cell for each indicator.

TextArea word-wrap

TextArea defaults to WordWrap = true.

Current behavior:

  • when wrapping is enabled, horizontal offset is forced to 0
  • vertical scrolling still applies

Wrap behavior is cell-based and rune-based; it is not “word wrap” with whitespace-aware breakpoints.

Integrating TextArea with ScrollViewer (concrete API)

TextEditorBase implements IScrollable, so ScrollViewer can bind to it.

Recommended composition:

var editor = new TextArea(text);
var view = new ScrollViewer(editor);

When the content implements IScrollable:

  • scrollbars update the editor’s ScrollModel
  • ScrollViewer.HorizontalOffset/VerticalOffset remain in sync with the editor scroll offsets

Input and command behavior (current)

Text insertion

  • OnTextInput: inserts e.Text
  • OnPaste: inserts the pasted string
  • Enter: inserts \n if AcceptsReturn is enabled (multi-line)
  • Tab: inserts \t if AcceptTab is enabled

Selection

  • Shift + navigation extends selection.
  • Mouse drag extends selection.
  • Ctrl+A selects all.

Selection is linear only.

  • Arrow keys move by rune (TerminalTextUtility.GetNextRuneIndex/GetPreviousRuneIndex).
  • Ctrl+Left / Ctrl+Right move by “word” category (letters/digits/underscore vs whitespace vs other).
  • Home / End move to line boundary.
  • Ctrl+Home / Ctrl+End move to start/end of document in multi-line.
  • PageUp / PageDown move by viewport height in multi-line.

Delete operations

  • Backspace/Delete remove selection if present.
  • Otherwise remove rune (or word in certain Ctrl-modified cases in single-line paths).

Clipboard

  • Ctrl+C copies selection to Terminal.Clipboard.
  • Ctrl+X cuts selection to Terminal.Clipboard.
  • Ctrl+V pastes from Terminal.Clipboard.Text.

Emacs-like kill/yank parity

Implemented:

  • Ctrl+K: kill to end
  • Ctrl+U: kill to start
  • Ctrl+W: kill previous word
  • Ctrl+Y: yank (insert kill buffer)

Known limitations (intentional for V1)

The current foundation is intentionally simple. Missing pieces include:

  • Undo/redo and edit transactions.
  • Multi-caret, rectangular selection, virtual space.
  • Auto-scroll while selecting outside the viewport.
  • Token-aware wrapping and cached layout per line.
  • IME/composition support (only basic text input events).
  • A classification + decoration pipeline (syntax highlighting, diagnostics, search highlights, etc.).
  • Background services with snapshot version gating.

CodeEditor roadmap (what we need next)

Performance: document storage and incremental line index

For CodeEditor-scale workloads, introduce a new ITextDocument implementation:

  • piece table / rope / gap buffer (implementation choice)
  • incremental updates of the line index (update only impacted line range)
  • snapshots that do not require copying the entire text on every edit
  • a richer change event model (batched changes via BeginUpdate())

Compatibility constraint:

  • TextBox/TextArea must keep working with DynamicTextDocument.

Undo/redo

Add an undo manager (likely an internal controller layered on top of the document operations):

  • transaction grouping (typing merges, paste merges)
  • undo/redo commands (Ctrl+Z / Ctrl+Y or Ctrl+Shift+Z)

This must work with any ITextDocument including DynamicTextDocument.

Scrolling policy: caret-follow vs user scrolling

Today the editor keeps the caret visible aggressively.

For CodeEditor we need a configurable policy:

  • allow the user to scroll without “snap back to caret”
  • optionally re-follow caret on typing / navigation
  • support autoscroll while selecting outside viewport

This likely requires separating:

  • the scroll offsets controlled by user gesture
  • the “bring caret into view” behavior as a mode (or threshold-based)

Rendering extensibility: classification and layers

The current segment-writer hook is sufficient for masking, but CodeEditor requires:

  • per-token styling (classification spans -> CellStyle)
  • layered rendering (selection, current line, highlights, squiggles/underlines, etc.)
  • gutter rendering (line numbers, glyphs)
  • low allocation (cache layouts per line; recompute only when width/version changes)

Proposed direction:

  • keep TextEditorCore as the caret/selection/viewport engine
  • add a pluggable “classifier” and “renderer” abstraction used during render:
    • input: snapshot + viewport (rows) + wrap settings
    • output: styled segments + decoration overlays

CodeEditor chrome and UI composition

Implement CodeEditor as a control composed around the editor surface:

  • left gutter for line numbers + glyphs
  • folding controls
  • diagnostics markers
  • completion popup anchored to caret cell coordinates

Likely composition:

  • Grid/DockLayout combining:
    • gutter visual
    • editor surface visual
    • optional minimap / overlays

Background services and version gating

Add snapshot-based services runnable off-thread:

  • tokenizer / highlighter
  • diagnostics engine
  • search index

All service results must be version-gated:

  • include snapshot version in the result
  • discard stale results on the UI thread if the document version advanced

Checklist

Implemented

  • Shared engine (TextEditorCore) for TextBox and TextArea
  • ITextDocument + snapshots + line model (CRLF preserved)
  • Delegate-backed document for bindable Text
  • Mouse selection inside viewport
  • Soft wrap in multi-line editor
  • ScrollViewer integration via IScrollable / ScrollModel
  • Masked input implemented as a TextBox specialization via segment writer override

Next for CodeEditor

  • efficient document (piece table/rope) + incremental line index
  • undo/redo
  • classification + layered rendering pipeline + gutter
  • improved scrolling policy and selection autoscroll
  • background services with snapshot version gating