Skip to content

Commit

Permalink
Added ability to prevent textProcessors from running on initial assig…
Browse files Browse the repository at this point in the history
…nment of attributedText in Editor (#341)
  • Loading branch information
rajdeep authored Sep 21, 2024
1 parent 2fbbdc7 commit 46db750
Show file tree
Hide file tree
Showing 8 changed files with 120 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ class TextProcessorExampleViewController: ExamplesBaseViewController {
])

registerTextProcessors()

editor.attributedText = NSAttributedString(string: "test")
}

private func registerTextProcessors() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ class TypeaheadTextProcessor: TextProcessing {
return .exclusive
}

var isRunOnSettingText: Bool {
true
}

weak var delegate: TypeaheadTextProcessorDelegate?
var triggerDeleted = false

Expand Down
6 changes: 4 additions & 2 deletions Proton/Sources/Swift/Core/RichTextEditorContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ class RichTextEditorContext: RichTextViewContext {

updateTypingAttributes(editor: editor, editedRange: range)

for processor in richTextView.textProcessor?.sortedProcessors ?? [] {
let executableProcessors = richTextView.textProcessor?.filteringExecutableOn(editor: editor) ?? []
for processor in executableProcessors {
let shouldProcess = processor.shouldProcess(editor, shouldProcessTextIn: range, replacementText: replacementText)
if shouldProcess == false {
return false
Expand Down Expand Up @@ -168,7 +169,8 @@ class RichTextEditorContext: RichTextViewContext {
private func invokeDidProcessIfRequired(_ richTextView: RichTextView) {
guard let editor = richTextView.superview as? EditorView else { return }

for processor in richTextView.textProcessor?.sortedProcessors ?? [] {
let executableProcessors = richTextView.textProcessor?.filteringExecutableOn(editor: editor) ?? []
for processor in executableProcessors {
processor.didProcess(editor: editor)
}
}
Expand Down
5 changes: 3 additions & 2 deletions Proton/Sources/Swift/Editor/EditorView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ open class EditorView: UIView {
public var asyncTextResolvers: [AsyncTextResolving] = []

/// Low-tech lock mechanism to know when `attributedText` is being set
private var isSettingAttributedText = false
private(set) var isSettingAttributedText = false


// Making this a convenience init fails the test `testRendersWidthRangeAttachment` as the init of a class subclassed from
Expand Down Expand Up @@ -1491,7 +1491,8 @@ extension EditorView: RichTextViewDelegate {
}

func richTextView(_ richTextView: RichTextView, selectedRangeChangedFrom oldRange: NSRange?, to newRange: NSRange?) {
textProcessor?.activeProcessors.forEach { $0.selectedRangeChanged(editor: self, oldRange: oldRange, newRange: newRange) }
let executableProcessors = textProcessor?.filteringExecutableOn(editor: self) ?? []
executableProcessors.forEach { $0.selectedRangeChanged(editor: self, oldRange: oldRange, newRange: newRange) }
}

func richTextView(_ richTextView: RichTextView, didTapAtLocation location: CGPoint, characterRange: NSRange?) {
Expand Down
4 changes: 4 additions & 0 deletions Proton/Sources/Swift/TextProcessors/TextProcessing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ public protocol TextProcessing {
/// executed. It is responsibility of these `TextProcessors` to do any cleanup/rollback if that needs to be done.
var priority: TextProcessingPriority { get }

var isRunOnSettingText: Bool { get }

/// Determines if the text should be changed in the editor.
/// - Note:
/// This function is invoked just before making the changes in the `EditorView`. Besides preventing changing text in Editor in certain cases,
Expand Down Expand Up @@ -124,6 +126,8 @@ public protocol TextProcessing {
}

public extension TextProcessing {
var isRunOnSettingText: Bool { true }

func handleKeyWithModifiers(editor: EditorView, key: EditorKey, modifierFlags: UIKeyModifierFlags, range editedRange: NSRange) { }
func selectedRangeChanged(editor: EditorView, oldRange: NSRange?, newRange: NSRange?) { }
func didProcess(editor: EditorView) { }
Expand Down
22 changes: 17 additions & 5 deletions Proton/Sources/Swift/TextProcessors/TextProcessor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ class TextProcessor: NSObject, NSTextStorageDelegate {
sortedProcessors = activeProcessors.sorted { $0.priority > $1.priority }
}
}
private(set) var sortedProcessors = [TextProcessing]()
private var sortedProcessors = [TextProcessing]()

weak var editor: EditorView?

init(editor: EditorView) {
Expand All @@ -52,22 +53,30 @@ class TextProcessor: NSObject, NSTextStorageDelegate {
}
}

func filteringExecutableOn(editor: EditorView) -> [TextProcessing] {
guard editor.isSettingAttributedText else { return sortedProcessors }
return sortedProcessors.filter { $0.isRunOnSettingText }
}

func textStorage(_ textStorage: NSTextStorage, willProcessEditing editedMask: NSTextStorage.EditActions, range editedRange: NSRange, changeInLength delta: Int) {
guard let editor = editor else { return }
var executedProcessors = [TextProcessing]()
var processed = false
let changedText = textStorage.substring(from: editedRange)

let editedMask = getEditedMask(delta: delta)
sortedProcessors.forEach {

let executableProcessors = filteringExecutableOn(editor: editor)

executableProcessors.forEach {
$0.willProcessEditing(editor: editor, editedMask: editedMask, range: editedRange, changeInLength: delta)
}

// This func is invoked even when selected range changes without change in text. Guard the code so that delegate call backs are
// fired only when there is actual change in content
guard delta != 0 else { return }

for processor in sortedProcessors {
for processor in executableProcessors {
if changedText == "\n" {
processor.handleKeyWithModifiers(editor: editor, key: .enter, modifierFlags: [], range: editedRange)
} else if changedText == "\t" {
Expand All @@ -89,14 +98,17 @@ class TextProcessor: NSObject, NSTextStorageDelegate {
func textStorage(_ textStorage: NSTextStorage, didProcessEditing editedMask: NSTextStorage.EditActions, range editedRange: NSRange, changeInLength delta: Int) {
guard let editor = editor else { return }
let editedMask = getEditedMask(delta: delta)
sortedProcessors.forEach {
let executableProcessors = filteringExecutableOn(editor: editor)

executableProcessors.forEach {
$0.didProcessEditing(editor: editor, editedMask: editedMask, range: editedRange, changeInLength: delta)
}
}

func textStorage(_ textStorage: NSTextStorage, willProcessDeletedText deletedText: NSAttributedString, insertedText: NSAttributedString, range: NSRange) {
guard let editor else { return }
for processor in sortedProcessors {
let executableProcessors = filteringExecutableOn(editor: editor)
for processor in executableProcessors {
processor.willProcess(editor: editor, deletedText: deletedText, insertedText: insertedText, range: range)
}
}
Expand Down
1 change: 1 addition & 0 deletions Proton/Tests/TextProcessors/Mocks/MockTextProcessor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import Proton
class MockTextProcessor: TextProcessing {
let name: String
var priority: TextProcessingPriority = .medium
var isRunOnSettingText: Bool = true

var onWillProcess: ((EditorView, NSAttributedString, NSAttributedString, NSRange) -> Void)?
var onProcess: ((EditorView, NSRange, Int) -> Void)?
Expand Down
90 changes: 85 additions & 5 deletions Proton/Tests/TextProcessors/TextProcessorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,26 @@ import XCTest

class TextProcessorTests: XCTestCase {
func testRegistersTextProcessor() {
let textProcessor = TextProcessor(editor: EditorView())
let editor = EditorView()
let textProcessor = TextProcessor(editor: editor)
let name = "TextProcessorTest"
let mockProcessor = MockTextProcessor(name: name)
textProcessor.register(mockProcessor)

XCTAssertEqual(textProcessor.sortedProcessors.count, 1)
XCTAssertEqual(textProcessor.sortedProcessors[0].name, name)
XCTAssertEqual(textProcessor.filteringExecutableOn(editor: editor).count, 1)
XCTAssertEqual(textProcessor.activeProcessors[0].name, name)
}

func testUnregistersTextProcessor() {
let textProcessor = TextProcessor(editor: EditorView())
let editor = EditorView()
let textProcessor = TextProcessor(editor: editor)
let name = "TextProcessorTest"
let mockProcessor = MockTextProcessor(name: name)
textProcessor.register(mockProcessor)

textProcessor.unregister(mockProcessor)

XCTAssertEqual(textProcessor.sortedProcessors.count, 0)
XCTAssertEqual(textProcessor.filteringExecutableOn(editor: editor).count, 0)
}

func testInvokesWillProcess() throws {
Expand Down Expand Up @@ -73,6 +75,69 @@ class TextProcessorTests: XCTestCase {
waitForExpectations(timeout: 1.0)
}

func testExecutesWillProcessOnSetAttributedText() throws {
let expectation = expectation(description: "Wait for willProcess to be invoked")
try assertProcessorInvocationOnSetAttributedText(expectation, isRunOnSettingText: true) { mockProcessor in
mockProcessor.onWillProcess = { _, _, _, _ in
expectation.fulfill()
}
}
waitForExpectations(timeout: 1.0)
}

func testExecutesWillProcessEditingOnSetAttributedText() throws {
let expectation = expectation(description: "Wait for willProcess to be invoked")
try assertProcessorInvocationOnSetAttributedText(expectation, isRunOnSettingText: true) { mockProcessor in
mockProcessor.willProcessEditing = { _, _, _, _ in
expectation.fulfill()
}
}
waitForExpectations(timeout: 1.0)
}

func testExecutesDidProcessEditingOnSetAttributedText() throws {
let expectation = expectation(description: "Wait for didProcess to be invoked")
try assertProcessorInvocationOnSetAttributedText(expectation, isRunOnSettingText: true) { mockProcessor in
mockProcessor.didProcessEditing = { _, _, _, _ in
expectation.fulfill()
}
}
waitForExpectations(timeout: 1.0)
}

func testPreventsWillProcessOnSetAttributedText() throws {
let expectation = expectation(description: "Should not wait for WillProcess to be invoked")
expectation.isInverted = true
try assertProcessorInvocationOnSetAttributedText(expectation, isRunOnSettingText: false) { mockProcessor in
mockProcessor.onWillProcess = { _, _, _, _ in
expectation.fulfill()
}
}
waitForExpectations(timeout: 1.0)
}

func testPreventsWillProcessEditingOnSetAttributedText() throws {
let expectation = expectation(description: "Should not wait for WillProcess to be invoked")
expectation.isInverted = true
try assertProcessorInvocationOnSetAttributedText(expectation, isRunOnSettingText: false) { mockProcessor in
mockProcessor.willProcessEditing = { _, _, _, _ in
expectation.fulfill()
}
}
waitForExpectations(timeout: 1.0)
}

func testPreventsDidProcessEditingOnSetAttributedText() throws {
let expectation = expectation(description: "Should not wait for DidProcess to be invoked")
expectation.isInverted = true
try assertProcessorInvocationOnSetAttributedText(expectation, isRunOnSettingText: false) { mockProcessor in
mockProcessor.didProcessEditing = { _, _, _, _ in
expectation.fulfill()
}
}
waitForExpectations(timeout: 1.0)
}

func testInvokesTextProcessor() {
let testExpectation = functionExpectation()
let editor = EditorView()
Expand Down Expand Up @@ -461,6 +526,21 @@ class TextProcessorTests: XCTestCase {

waitForExpectations(timeout: 1.0)
}

private func assertProcessorInvocationOnSetAttributedText(_ expectation: XCTestExpectation, isRunOnSettingText: Bool, file: StaticString = #file, line: UInt = #line, assertion: (MockTextProcessor) -> Void) throws {
let editor = EditorView()
editor.forceApplyAttributedText = true

let name = "TextProcessorTest"
let mockProcessor = MockTextProcessor(name: name)
mockProcessor.isRunOnSettingText = isRunOnSettingText
assertion(mockProcessor)

let testString = NSAttributedString(string: "test some text")
editor.registerProcessor(mockProcessor)

editor.attributedText = testString
}
}

extension EditorView {
Expand Down

0 comments on commit 46db750

Please sign in to comment.