struct TextEditingState
Swift
struct TextEditingState
Selection and caret for a single editable string.
anchor is where a selection started, focus is the moving end and where
the caret is drawn. They are equal when there is no selection.
Sources/LavaText/TextEditingState.swift:19
Constructors
init(_ text: String = "")
Creates a state holding text, with the caret at the start.
Sources/LavaText/TextEditingState.swift:138
Properties
var affinity: CaretAffinity { get }
Which side of a wrap boundary the caret sits on. Only meaningful when
the caret is exactly at one; see CaretAffinity.
Sources/LavaText/TextEditingState.swift:83
var anchor: String.Index { get }
Where the selection started: the end that stays put while it is extended.
Sources/LavaText/TextEditingState.swift:23
var canRedo: Bool { get }
Whether there is an undone edit to redo.
Sources/LavaText/TextEditingState.swift:429
var canUndo: Bool { get }
Whether there is an edit to undo.
Sources/LavaText/TextEditingState.swift:427
var focus: String.Index { get }
The moving end of the selection, where the caret is drawn.
Sources/LavaText/TextEditingState.swift:25
var hasSelection: Bool { get }
Whether anything is selected.
Sources/LavaText/TextEditingState.swift:148
var isMultiline: Bool { get }
Whether the buffer contains any hard line break.
Sources/LavaText/TextEditingState.swift:679
var layout: VisualLayout { get }
Row structure currently in effect.
Sources/LavaText/TextEditingState.swift:91
var lines: [Substring] { get }
Logical lines, split on "\n". A trailing newline yields a final empty line, which is correct: the caret can sit there.
Collection.split compares Characters, so it grapheme-breaks the
whole buffer to find bytes it could have scanned for — on a 4 MB log
that is ~60 ms, and a wrapping editor asks for this on every keystroke.
The scan below finds the newlines as bytes and cuts the same
Substrings at them.
Sources/LavaText/TextEditingState.swift:689
var revision: UInt64 { get }
Which version of the content this is. Changes on every edit, and never for a caret or selection move.
Exists for the caches around this type. Converting between a
String.Index and a character offset walks the buffer, so every
consumer that does it in a hot path — the draw list's first visible
row, the caret, a hit test — keeps the last pair it resolved and works
relative to it. Such a pair is only meaningful for the buffer it was
taken from, and "has the text changed" has no cheap answer otherwise:
comparing two Strings is a content comparison, and comparing a copy
of the state (this is a value type, copied constantly) says nothing
about whether the text moved.
Process-wide unique rather than a per-instance counter, so a cache cannot mistake a freshly constructed state — whose counter would start over — for the one it sampled.
It participates in the synthesized Equatable, which makes equality
stricter than it was: two states built separately from the same string
are no longer ==. Nothing in the repo compares whole states (only
their text), and leaving the conformance synthesized is worth more
than the looser semantics — a hand-written == is one field away from
silently ignoring the next thing added here.
Sources/LavaText/TextEditingState.swift:50
var selectedRange: Range<String.Index> { get }
Selection in document order, regardless of drag direction.
Sources/LavaText/TextEditingState.swift:151
var selectedText: String { get }
The selected text; empty when nothing is selected.
Sources/LavaText/TextEditingState.swift:156
var text: String { get }
The buffer.
Sources/LavaText/TextEditingState.swift:21
var undoStack: UndoStack { get }
Edit history. Every mutation funnels through replace(...), which is
what makes undo possible without auditing each operation separately —
the thing the plan warned becomes painful to retrofit.
Sources/LavaText/TextEditingState.swift:66
var visualRows: [Range<Int>]? { get }
Visual rows, when the view has wrapped the buffer. Nil means one row per logical line. Vertical movement and Home/End follow these — with wrapping, a visual row is not a logical line, and navigating by newlines would skip whole wrapped rows.
Sources/LavaText/TextEditingState.swift:79
Methods
mutating func clearSelection()
Collapses to the caret end, as typing or a plain arrow key should.
Sources/LavaText/TextEditingState.swift:203
func column(of index: String.Index) -> Int
Character offset of index within its own line.
Sources/LavaText/TextEditingState.swift:756
mutating func deleteBackward()
Deletes the selection, or the character before the caret (Backspace).
Sources/LavaText/TextEditingState.swift:478
mutating func deleteForward()
Deletes the selection, or the character after the caret (Delete).
Sources/LavaText/TextEditingState.swift:490
mutating func deleteWordBackward()
Deletes the selection, or back to the start of the previous word (Ctrl+Backspace).
Sources/LavaText/TextEditingState.swift:500
func index(atOffset offset: Int) -> String.Index
Clamped with limitedBy: rather than against text.count, because
String.count is a full grapheme-break walk of the buffer — O(length)
regardless of how small offset is. The draw path calls this once per
emit to reach the first visible row, so on a 10 MB log that single
clamp cost ~12ms of every redraw, including ones that only moved a
caret. limitedBy: costs O(min(offset, length)) and stops at the end.
Sources/LavaText/TextEditingState.swift:576
func index(line: Int, column: Int) -> String.Index
Index at line/column, clamped to that line's length.
Sources/LavaText/TextEditingState.swift:779
mutating func insert(_ string: String)
Replaces the selection with string, or inserts it at the caret.
Sources/LavaText/TextEditingState.swift:472
func lineIndex(of index: String.Index) -> Int
Zero-based logical line number of index.
Sources/LavaText/TextEditingState.swift:745
func lineRange(at index: String.Index) -> Range<String.Index>
Range of the line containing index, excluding its terminator.
Sources/LavaText/TextEditingState.swift:730
mutating func moveDown(extending: Bool = false)
Moves down one row, keeping the column the vertical movement started from. From the last row, moves to the end of the buffer.
Sources/LavaText/TextEditingState.swift:840
mutating func moveLeft(extending: Bool = false)
Moves the caret one character left. Without extending, a selection
collapses to its start instead.
Sources/LavaText/TextEditingState.swift:218
mutating func moveRight(extending: Bool = false)
Moves the caret one character right. Without extending, a selection
collapses to its end instead.
Sources/LavaText/TextEditingState.swift:236
mutating func moveToEnd(extending: Bool = false)
Moves the caret to the end of the buffer.
Sources/LavaText/TextEditingState.swift:259
mutating func moveToLineEnd(extending: Bool = false)
Moves the caret to the end of its visual row (End).
Sources/LavaText/TextEditingState.swift:801
mutating func moveToLineStart(extending: Bool = false)
Home/End act on the visual row, matching what the user sees. When nothing is wrapped this is the same as the logical line.
Sources/LavaText/TextEditingState.swift:790
mutating func moveToStart(extending: Bool = false)
Moves the caret to the start of the buffer.
Sources/LavaText/TextEditingState.swift:251
mutating func moveUp(extending: Bool = false)
Moves up one line, preserving the column the user started from.
The remembered column is the whole point. Without it, stepping down through a short line and back up strands the caret at that short line's end instead of returning to the original column — the classic multi-line caret bug, and invisible until someone navigates ragged text.
Sources/LavaText/TextEditingState.swift:819
mutating func moveWordLeft(extending: Bool = false)
Moves the caret to the start of the word before it, as Ctrl+Left does.
Sources/LavaText/TextEditingState.swift:267
mutating func moveWordRight(extending: Bool = false)
Moves the caret past the end of the word after it, as Ctrl+Right does.
Sources/LavaText/TextEditingState.swift:275
func offset(of index: String.Index) -> Int
Character offset of an index. Characters, not bytes, so the value
stays meaningful across graphemes of differing byte length.
Character offset of index, measured from the last pair resolved.
Both conversions here walk graphemes, so unaided they cost the offset, not the distance moved. Every vertical move makes one of each, so a caret near the end of a large file paid for the whole file to move one row — and Page Down, being a row move repeated a screenful of times, paid it thirty-odd times over. On a 4.7 MB buffer: 0.68ms per page at the top, 187ms twenty-seven thousand rows in, rising linearly and without bound. That is the reported symptom exactly — "the farther from the beginning, the laggier".
Anchored, the cost is the distance travelled: a row for an arrow key, a
screenful for a page, and flat with depth. LeafNode already does this
for the draw and drag paths; here it is where the caret itself lives,
so every caller gets it rather than the two that remembered to.
Sources/LavaText/TextEditingState.swift:555
@discardableResult mutating func redo() -> Bool
Redoes the last undone edit. Returns false when there is nothing to redo.
Sources/LavaText/TextEditingState.swift:445
mutating func replace(offsets range: Range<Int>, with replacement: String)
Replaces a character range with replacement, as one undoable edit.
Offsets rather than String.Index because the callers are find/replace
and an LSP-style edit, and both of those hold offsets from a scan that
has already finished — see TextSearch.
Sources/LavaText/TextEditingState.swift:370
@discardableResult mutating func replaceAll(_ ranges: [Range<Int>], with replacement: String) -> Int
Replaces every range in ranges with replacement and returns how
many were applied. Ranges are character offsets over the current
buffer, as TextSearch.matches reports them.
One undo step, not one per match. That is the whole reason this is
here rather than in the caller: a replace-all is a single thing the
user did, and undoing it a match at a time is the behaviour people file
bugs about. It is done by rewriting the span from the first match to
the last as a single edit, so the recorded removed text is that whole
span — replacing across a large buffer holds a copy of it in the undo
history until the history is dropped.
Overlapping ranges are not applied twice: after sorting, any range
starting before the previous one ended is skipped. Empty ranges are
skipped too — a zero-width match would otherwise insert replacement
at every position between two real ones.
Sources/LavaText/TextEditingState.swift:393
mutating func selectAll()
Selects the whole buffer.
Sources/LavaText/TextEditingState.swift:195
mutating func selectWord(at index: String.Index)
Selects the word containing index — what a double-click should do.
Falls back to the run of separators when the click lands between words,
so a double-click on whitespace still selects something coherent.
Sources/LavaText/TextEditingState.swift:161
mutating func setCursor(_ index: String.Index, extending: Bool = false)
Moves the caret to index, clamped to the buffer. With extending, the
anchor stays and the selection grows or shrinks to meet it.
Sources/LavaText/TextEditingState.swift:207
mutating func setText(_ new: String, keepingCursor: Bool = false)
Replaces the whole buffer, e.g. when the bound value changed elsewhere.
Sources/LavaText/TextEditingState.swift:517
mutating func setVisualRows(_ rows: [Range<Int>]?)
Installed by the view after each wrap pass.
Sources/LavaText/TextEditingState.swift:86
func startOfLine(_ n: Int) -> String.Index
Sources/LavaText/TextEditingState.swift:761
@discardableResult mutating func undo() -> Bool
Undoes the last edit, putting the selection back where it was before it.
Returns false when there is nothing to undo.
Sources/LavaText/TextEditingState.swift:434
func wordRange(at index: String.Index) -> Range<String.Index>
The word-ish run around index, using the same classification as
Ctrl+arrow so double-click and word movement agree.
Sources/LavaText/TextEditingState.swift:171
Operators
static func == (lhs: TextEditingState, rhs: TextEditingState) -> Bool
Compares text, selection, history and layout. The index caches are left out.
Sources/LavaText/TextEditingState.swift:130