Skip to content

Commit

Permalink
Factor out undo support, update API name
Browse files Browse the repository at this point in the history
  • Loading branch information
mattmassicotte committed Jan 7, 2025
1 parent 74bb61f commit ba2dc31
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 40 deletions.
4 changes: 2 additions & 2 deletions IBeamTextViewSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public struct IBeamTextViewSystem {
}
}

extension IBeamTextViewSystem : @preconcurrency IBeam.TextSystem {
extension IBeamTextViewSystem : @preconcurrency IBeam.TextSystemInterface {
public typealias TextRange = NSRange
public typealias TextPosition = Int

Expand Down Expand Up @@ -86,6 +86,6 @@ extension IBeamTextViewSystem : @preconcurrency IBeam.TextSystem {
public func endEditing() { partialSystem.endEditing() }

public func applyMutation(_ range: TextRange, string: AttributedString) -> MutationOutput<TextRange>? {
partialSystem.applyMutation(range, string: string)
partialSystem.applyMutation(range, string: string, undoManager: nil)
}
}
6 changes: 3 additions & 3 deletions Sources/IBeam/MockTextSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ public final class MockTextSystem : TextSystemInterface {
case boundingRect(CGRect?)
}

private var partialSystem: MutableStringPartialSystem
private var partialSystem: MutableStringPartialInterface
public var responses: [Response] = []

public init(_ string: NSAttributedString) {
self.partialSystem = MutableStringPartialSystem(NSMutableAttributedString(attributedString: string))
self.partialSystem = MutableStringPartialInterface(NSMutableAttributedString(attributedString: string))
}

public convenience init(_ string: String) {
Expand Down Expand Up @@ -94,6 +94,6 @@ public final class MockTextSystem : TextSystemInterface {
}

public func applyMutation(_ range: TextRange, string: AttributedString) -> MutationOutput<TextRange>? {
partialSystem.applyMutation(in: range, string: string, undoManager: nil)
partialSystem.applyMutation(range, string: string, undoManager: nil)
}
}
59 changes: 40 additions & 19 deletions Sources/IBeam/MutableStringPartialSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,35 @@ import AppKit
import UIKit
#endif

/// Implements a large portion of the TextSystem protocol for NSMutableAttributedString-compatible backing stores.
public struct MutableStringPartialSystem {
private var content: NSMutableAttributedString

extension NSMutableAttributedString {
/// Compute and register the inverse mutation required to undo replacing the content within `range`.
public func registerMutationUndo(
with undoManager: UndoManager?,
range: NSRange,
delta: Int
) {
guard let undoManager else {
return
}

// while this is technically cheating, I believe it to be safe
nonisolated(unsafe) let existingString = attributedSubstring(from: range)
let newLength = existingString.length + max(delta, 0)

precondition(newLength > 0)

let inverseRange = NSRange(location: range.location, length: newLength)

undoManager.registerUndo(withTarget: self, handler: { target in
target.replaceCharacters(in: inverseRange, with: existingString)
})
}
}

/// Implements a large portion of the T`extSystemInterface` protocol for `NSMutableAttributedString`-compatible backing stores.
public struct MutableStringPartialInterface {
private let content: NSMutableAttributedString

public init(_ content: NSMutableAttributedString) {
self.content = content
}
Expand All @@ -23,7 +48,7 @@ public struct MutableStringPartialSystem {
}
}

extension MutableStringPartialSystem {
extension MutableStringPartialInterface {
public func position(from start: Int, offset: Int) -> Int? {
start + offset
}
Expand Down Expand Up @@ -86,22 +111,12 @@ extension MutableStringPartialSystem {
content.endEditing()
}

public func applyMutation(in range: NSRange, string: AttributedString, undoManager: UndoManager?) -> MutationOutput<NSRange> {
let nsAttrString = NSAttributedString(string)
let length = nsAttrString.length

if let undoManager {
let existingString = AttributedString(content.attributedSubstring(from: range))
let inverseRange = NSRange(location: range.location, length: length)
public func applyMutation(_ range: NSRange, string: NSAttributedString, undoManager: UndoManager?) -> MutationOutput<NSRange>? {
let length = string.length

undoManager.registerUndo(withTarget: content, handler: { target in
let existingNSAttrString = NSAttributedString(existingString)

target.replaceCharacters(in: inverseRange, with: existingNSAttrString)
})
}
content.registerMutationUndo(with: undoManager, range: range, delta: length - range.length)

content.replaceCharacters(in: range, with: nsAttrString)
content.replaceCharacters(in: range, with: string)

let delta = length - range.length
let position = min(range.lowerBound + length, content.length)
Expand All @@ -110,4 +125,10 @@ extension MutableStringPartialSystem {

return MutationOutput<NSRange>(selection: newSelection, delta: delta)
}

public func applyMutation(_ range: NSRange, string: AttributedString, undoManager: UndoManager?) -> MutationOutput<NSRange>? {
let nsAttrString = NSAttributedString(string)

return applyMutation(range, string: nsAttrString, undoManager: undoManager)
}
}
28 changes: 28 additions & 0 deletions Sources/IBeam/TextSystemInterface.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,31 @@ extension TextSystemInterface {
return Cursor<TextRange>(range, alignment: alignment)
}
}

extension TextSystemInterface where Self: AnyObject, TextRange: Sendable {
public func registerMutationUndo(
with undoManager: UndoManager?,
range: TextRange,
substringProvider: (TextRange) -> (AttributedString, Int)?
) {
guard
let undoManager,
let (existing, length) = substringProvider(range)
else {
return
}

let start = positions(composing: range).0

guard
let end = position(from: start, offset: length),
let inverseRange = textRange(from: start, to: end)
else {
return
}

undoManager.registerUndo(withTarget: self, handler: { target in
_ = target.applyMutation(inverseRange, string: existing)
})
}
}
16 changes: 0 additions & 16 deletions Sources/IBeam/UndoManager+MainActor.swift

This file was deleted.

0 comments on commit ba2dc31

Please sign in to comment.