LavaUI docs

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

Start index of line n, or endIndex if n is past the last line.

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