Transform text editing operations into precise line-based diffs for array-backed text models.
TextActions bridges the gap between text views and array-based document models by analyzing text changes and producing structured diffs with exact line indexes for insertions, deletions, and edits.
When you have a text view displaying content from an array-based model (like an array of strings representing lines), changes made in the text view need to be translated back to the underlying model. TextActions does exactly that - it takes a text editing operation (like typing, deleting, or pasting) and tells you precisely which array elements need to be inserted, deleted, or modified.
- ๐ฏ Precise Analysis: Converts text editing operations to exact line-based array operations
- โก High Performance: Uses binary search and efficient algorithms for line indexing
- ๐ง Smart Logic: Handles complex scenarios like multi-line pastes, line merging, and boundary cases
- ๐ฑ Cross-Platform: Works on macOS 11+, iOS 13+, and other Swift platforms
- ๐ Well-Tested: Comprehensive test suite with hundreds of edge cases
- ๐๏ธ Foundation-Light: Minimal dependencies for maximum compatibility
import TextActions
let originalText = """
Line 1
Line 2
Line 3
"""
// Create an indexed view of the text
let indexedString = IndexedString(contents: originalText)
// User selects "Line 2\n" and types "New Line\nAnother Line\n"
let selectedRange = NSRange(location: 7, length: 7) // "Line 2\n"
let replacementString = "New Line\nAnother Line\n"
// Analyze what needs to change in your array model
let action = TextEditingActionAnalyzer.textEditingActionGiven(
selectedRange: selectedRange,
replacementString: replacementString,
in: indexedString
)
// Results tell you exactly what to do:
print(action.kind) // .editMultipleLines
print(action.linesEdited) // [1] (index 1 gets edited)
print(action.linesInserted) // [2] (insert new line at index 2)
print(action.linesDeleted) // [] (no lines deleted)
// Your array transformation:
// lines[1] = "New Line"
// lines.insert("Another Line", at: 2)An efficient wrapper around strings that provides line-based indexing and fast lookups:
let indexedString = IndexedString(contents: multilineText)
// Access lines by index
let firstLine = indexedString[0, .LineContents] // Content without newline
let firstLineFull = indexedString[0, .Line] // Content with newline
// Find which line contains a character position
let lineIndex = indexedString.indexOfLineContainingLocation(50)
// Get line ranges
let lineRange = indexedString.rangeOfLineAtIndex(2)The result of analyzing a text editing operation:
public enum TextEditingActionKind {
case insertSingleLine // Adding one new line
case insertMultipleLines // Adding multiple new lines
case removeSingleLine // Removing one line
case removeMultipleLines // Removing multiple lines
case editSingleLine // Modifying content of one line
case editMultipleLines // Complex edit affecting multiple lines
case replaceAll // Selecting all and replacing
case clearAll // Selecting all and deleting
case unidentifiedAction // Fallback case
}
// Each action contains precise IndexSets:
action.linesEdited // Which existing lines need content updates
action.linesInserted // Which new lines to insert (and where)
action.linesDeleted // Which existing lines to removeTextActions excels at handling complex editing scenarios:
// Multi-line paste over a selection
let selectedRange = NSRange(location: 10, length: 25) // Spans multiple lines
let pastedText = "First\nSecond\nThird"
let action = TextEditingActionAnalyzer.textEditingActionGiven(
selectedRange: selectedRange,
replacementString: pastedText,
in: indexedString
)
// Results might be:
// - action.linesEdited: [1, 3] (modify first and last affected lines)
// - action.linesInserted: [2] (insert new line in middle)
// - action.linesDeleted: [4, 5] (remove lines that were fully selected)Implement TextEditingActionContext for custom text representations:
extension MyTextModel: TextEditingActionContext {
public var lineCount: Int { lines.count }
public func indexesOfLinesTouchedBy(range: NSRange) -> IndexSet {
// Your line detection logic
}
public func rangeOfLineAt(index: Int, includeEndOfLine: Bool) -> NSRange {
// Your range calculation logic
}
// ... implement other required methods
}
// Now you can analyze edits directly on your model
let action = TextEditingActionAnalyzer.textEditingActionGiven(
selectedRange: range,
replacementString: text,
in: myTextModel
)Add TextActions to your project via Xcode:
- File โ Add Package Dependencies...
- Enter:
https://github.com/YourUsername/TextActions
Or add it to your Package.swift:
dependencies: [
.package(url: "https://github.com/YourUsername/TextActions", from: "1.0.0")
]TextActions is perfect for:
- ๐ Text Editors: Apps with line-based text editing (code editors, note apps)
- ๐ Document Models: Converting text changes to structured document updates
- ๐ Undo/Redo Systems: Tracking precise changes for granular undo operations
- ๐ฑ Collaborative Editing: Converting user edits to operational transforms
- ๐งฎ Calculation Apps: Text-based interfaces backed by expression arrays (like Soulver)
- ๐ List Editors: Any app where text view content maps to array elements
- Swift 5.9+
- macOS 11.0+ / iOS 13.0+
- No external dependencies beyond Foundation
TextActions is built for performance:
- Binary Search: O(log n) line lookups using efficient caching
- Minimal Allocations: Reuses data structures where possible
- Smart Caching: Line metrics calculated once and cached
- Foundation-Light: Minimal dependency footprint
We welcome contributions! Please:
- Fork the repository
- Create a feature branch
- Add tests for your changes
- Ensure all tests pass
- Submit a pull request
TextActions is available under the MIT license. See LICENSE for details.
Originally developed by Zac Cohan as part of the Soulver calculation engine. Extracted into a standalone package to benefit the broader Swift community.
Transform your text editing operations into precise array diffs with TextActions.