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:
TextBox / TextArea while enabling future growth.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)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)ScrollModel (src/XenoAtom.Terminal.UI/Scrolling/ScrollModel.cs)IScrollable (src/XenoAtom.Terminal.UI/Scrolling/IScrollable.cs)ScrollViewer (src/XenoAtom.Terminal.UI/Controls/ScrollViewer.cs)WordWrap).ICursorProvider (caret is not rendered in-buffer).Measure(in LayoutConstraints) / Arrange(Rectangle) and SizeHints.CellBuffer and clipped by the renderer (no “render all then clip” hacks).OnKeyDown, OnTextInput, OnPointer*, OnPaste).Terminal.Clipboard.ITextDocument and versioningITextDocument provides:
CurrentSnapshot and VersionInsert, Remove, ReplaceBeginUpdate() (currently does not batch change events; reserved for future batching)Changed event (TextDocumentChangedEventArgs)Current contract assumptions:
Version increments when text changes.Version changes.TextSnapshot contains:
string textLineStarts[] and per-line LineBreakLength[] (so CRLF vs LF is preserved)TextLine exposes:
Start, Length, LineBreakLengthEnd and EndIncludingBreakText with DynamicTextDocumentTextEditorBase 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:
ITextDocument.DynamicTextDocument writes back into the bindable Text property.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.TextEditorBase + TextEditorCoreTextEditorBase responsibilitiesTextEditorBase is a Visual that:
TextEditorCoreScrollModel and exposes it via IScrollableTextEditorCoreICursorProviderPlaceholderAcceptTabWordWrapITextDocument TextDocument (public, swappable)TextEditorCore responsibilitiesTextEditorCore is internal and currently owns:
anchor + end)document.VersionRendering 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).
Caret rendering is out-of-band:
TextEditorCore.TryGetCursorCell(...) computes (x,y) in terminal cells.TerminalApp) based on the focused ICursorProvider.ScrollModel)The editor sets:
ScrollModel.ScrollToMakeVisible(...) is used to keep the caret visible.
TextBox overflow indicators (horizontal)TextBox uses ScrollModel.OffsetX and supports optional overflow indicators via style:
Scroll.OffsetX > 0Scroll.OffsetX + ViewportWidth < ExtentWidthWhen indicators are visible, TextBox shrinks the editor rectangle to reserve one cell for each indicator.
TextArea word-wrapTextArea defaults to WordWrap = true.
Current behavior:
0Wrap behavior is cell-based and rune-based; it is not “word wrap” with whitespace-aware breakpoints.
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:
ScrollModelScrollViewer.HorizontalOffset/VerticalOffset remain in sync with the editor scroll offsetsOnTextInput: inserts e.TextOnPaste: inserts the pasted stringEnter: inserts \n if AcceptsReturn is enabled (multi-line)Tab: inserts \t if AcceptTab is enabledSelection is linear only.
TerminalTextUtility.GetNextRuneIndex/GetPreviousRuneIndex).Terminal.Clipboard.Terminal.Clipboard.Terminal.Clipboard.Text.Implemented:
The current foundation is intentionally simple. Missing pieces include:
For CodeEditor-scale workloads, introduce a new ITextDocument implementation:
BeginUpdate())Compatibility constraint:
TextBox/TextArea must keep working with DynamicTextDocument.Add an undo manager (likely an internal controller layered on top of the document operations):
This must work with any ITextDocument including DynamicTextDocument.
Today the editor keeps the caret visible aggressively.
For CodeEditor we need a configurable policy:
This likely requires separating:
The current segment-writer hook is sufficient for masking, but CodeEditor requires:
CellStyle)Proposed direction:
TextEditorCore as the caret/selection/viewport engineImplement CodeEditor as a control composed around the editor surface:
Likely composition:
Grid/DockLayout combining:
Add snapshot-based services runnable off-thread:
All service results must be version-gated:
TextEditorCore) for TextBox and TextAreaITextDocument + snapshots + line model (CRLF preserved)TextIScrollable / ScrollModelTextBox specialization via segment writer override