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 = "")
Not documented.
Sources/LavaText/TextEditingState.swift:133
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:80
var anchor: String.Index { get }
Not documented.
Sources/LavaText/TextEditingState.swift:21
var canRedo: Bool { get }
Not documented.
Sources/LavaText/TextEditingState.swift:409
var canUndo: Bool { get }
Not documented.
Sources/LavaText/TextEditingState.swift:408
var focus: String.Index { get }
Not documented.
Sources/LavaText/TextEditingState.swift:22
var hasSelection: Bool { get }
Not documented.
Sources/LavaText/TextEditingState.swift:142
var isMultiline: Bool { get }
Whether the buffer contains any hard line break.
Sources/LavaText/TextEditingState.swift:652
var layout: VisualLayout { get }
Row structure currently in effect.
Sources/LavaText/TextEditingState.swift:88
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:662
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:47
var selectedRange: Range<String.Index> { get }
Selection in document order, regardless of drag direction.
Sources/LavaText/TextEditingState.swift:145
var selectedText: String { get }
Not documented.
Sources/LavaText/TextEditingState.swift:149
var text: String { get }
Not documented.
Sources/LavaText/TextEditingState.swift:20
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:63
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:76
Methods
mutating func clearSelection()
Collapses to the caret end, as typing or a plain arrow key should.
Sources/LavaText/TextEditingState.swift:195
func column(of index: String.Index) -> Int
Character offset of index within its own line.
Sources/LavaText/TextEditingState.swift:729
mutating func deleteBackward()
Not documented.
Sources/LavaText/TextEditingState.swift:453
mutating func deleteForward()
Not documented.
Sources/LavaText/TextEditingState.swift:464
mutating func deleteWordBackward()
Not documented.
Sources/LavaText/TextEditingState.swift:473
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:549
func index(line: Int, column: Int) -> String.Index
Index at line/column, clamped to that line's length.
Sources/LavaText/TextEditingState.swift:752
mutating func insert(_ string: String)
Not documented.
Sources/LavaText/TextEditingState.swift:448
func lineIndex(of index: String.Index) -> Int
Zero-based logical line number of index.
Sources/LavaText/TextEditingState.swift:718
func lineRange(at index: String.Index) -> Range<String.Index>
Range of the line containing index, excluding its terminator.
Sources/LavaText/TextEditingState.swift:703
mutating func moveDown(extending: Bool = false)
Not documented.
Sources/LavaText/TextEditingState.swift:810
mutating func moveLeft(extending: Bool = false)
Not documented.
Sources/LavaText/TextEditingState.swift:206
mutating func moveRight(extending: Bool = false)
Not documented.
Sources/LavaText/TextEditingState.swift:222
mutating func moveToEnd(extending: Bool = false)
Not documented.
Sources/LavaText/TextEditingState.swift:243
mutating func moveToLineEnd(extending: Bool = false)
Not documented.
Sources/LavaText/TextEditingState.swift:773
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:763
mutating func moveToStart(extending: Bool = false)
Not documented.
Sources/LavaText/TextEditingState.swift:236
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:791
mutating func moveWordLeft(extending: Bool = false)
Not documented.
Sources/LavaText/TextEditingState.swift:250
mutating func moveWordRight(extending: Bool = false)
Not documented.
Sources/LavaText/TextEditingState.swift:257
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:528
@discardableResult mutating func redo() -> Bool
Not documented.
Sources/LavaText/TextEditingState.swift:422
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:352
@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:375
mutating func selectAll()
Not documented.
Sources/LavaText/TextEditingState.swift:187
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:154
mutating func setCursor(_ index: String.Index, extending: Bool = false)
Not documented.
Sources/LavaText/TextEditingState.swift:197
mutating func setText(_ new: String, keepingCursor: Bool = false)
Replaces the whole buffer, e.g. when the bound value changed elsewhere.
Sources/LavaText/TextEditingState.swift:490
mutating func setVisualRows(_ rows: [Range<Int>]?)
Installed by the view after each wrap pass.
Sources/LavaText/TextEditingState.swift:83
func startOfLine(_ n: Int) -> String.Index
Sources/LavaText/TextEditingState.swift:734
@discardableResult mutating func undo() -> Bool
Not documented.
Sources/LavaText/TextEditingState.swift:412
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:164
Operators
static func == (lhs: TextEditingState, rhs: TextEditingState) -> Bool
Not documented.
Sources/LavaText/TextEditingState.swift:126