Skip to content

Commit

Permalink
Added additional delegate callbacks on TextProcesing to inform consum…
Browse files Browse the repository at this point in the history
…ers of changes to tex, attributes or both
  • Loading branch information
rajdeep committed Jul 10, 2024
1 parent 1698682 commit 3e0252f
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 1 deletion.
2 changes: 1 addition & 1 deletion Proton/Sources/ObjC/PRTextStorage.m
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ - (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str {
NSInteger delta = str.length - range.length;
[_storage replaceCharactersInRange:range withString:str];
[_storage fixAttributesInRange:NSMakeRange(0, _storage.length)];
[self edited:NSTextStorageEditedCharacters & NSTextStorageEditedAttributes range:range changeInLength:delta];
[self edited:NSTextStorageEditedCharacters | NSTextStorageEditedAttributes range:range changeInLength:delta];

[self endEditing];
}
Expand Down
18 changes: 18 additions & 0 deletions Proton/Sources/Swift/TextProcessors/TextProcessing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,29 @@ public protocol TextProcessing {
/// Invoked after the text has been processed in the `Editor`.
/// - Parameter editor: EditorView in which text is changed.
func didProcess(editor: EditorView)

/// Invoked when `editor` is about to process editing changes. The delegate can use this method to perform any necessary preparations before the changes are applied.
/// - Parameters:
/// - editor: The `EditorView` instance that is about to process editing changes.
/// - editedMask: `NSTextStorage.EditActions` indicating the types of edits that are about to be processed. This parameter can contain `.editedCharacters`, `.editedAttributes`, or both, indicating whether the changes involve modifications to the text characters, text attributes, or both.
/// - editedRange: Range of text that is affected by the editing changes. This range is specified in the coordinate system of the text storage's string.
/// - delta: Indicates the change in length of the text as a result of the editing. A positive value indicates an increase in length, while a negative value indicates a decrease. This may be used to adjust any related data structures that depend on the text length.
func willProcessEditing(editor: EditorView, editedMask: NSTextStorage.EditActions, range editedRange: NSRange, changeInLength delta: Int)

/// Invoked after `editor` has processed editing changes. The delegate can use this method to perform any necessary actions after the content in Editor has been committed following current edit action.
/// - Parameters:
/// - editor: The `EditorView` instance that is about to process editing changes.
/// - editedMask: `NSTextStorage.EditActions` indicating the types of edits that are about to be processed. This parameter can contain `.editedCharacters`, `.editedAttributes`, or both, indicating whether the changes involve modifications to the text characters, text attributes, or both.
/// - editedRange: Range of text that is affected by the editing changes. This range is specified in the coordinate system of the text storage's string.
/// - delta: Indicates the change in length of the text as a result of the editing. A positive value indicates an increase in length, while a negative value indicates a decrease. This may be used to adjust any related data structures that depend on the text length.
func didProcessEditing(editor: EditorView, editedMask: NSTextStorage.EditActions, range editedRange: NSRange, changeInLength delta: Int)
}

public extension TextProcessing {
func handleKeyWithModifiers(editor: EditorView, key: EditorKey, modifierFlags: UIKeyModifierFlags, range editedRange: NSRange) { }
func selectedRangeChanged(editor: EditorView, oldRange: NSRange?, newRange: NSRange?) { }
func didProcess(editor: EditorView) { }
func shouldProcess(_ editorView: EditorView, shouldProcessTextIn range: NSRange, replacementText text: String) -> Bool { return true }
func willProcessEditing(editor: EditorView, editedMask: NSTextStorage.EditActions, range editedRange: NSRange, changeInLength delta: Int) { }
func didProcessEditing(editor: EditorView, editedMask: NSTextStorage.EditActions, range editedRange: NSRange, changeInLength delta: Int) { }
}
12 changes: 12 additions & 0 deletions Proton/Sources/Swift/TextProcessors/TextProcessor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ class TextProcessor: NSObject, NSTextStorageDelegate {
var processed = false
let changedText = textStorage.substring(from: editedRange)

sortedProcessors.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 }
Expand Down Expand Up @@ -88,6 +92,14 @@ class TextProcessor: NSObject, NSTextStorageDelegate {
}
}

func textStorage(_ textStorage: NSTextStorage, didProcessEditing editedMask: NSTextStorage.EditActions, range editedRange: NSRange, changeInLength delta: Int) {
guard let editor = editor else { return }

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

private func notifyInterruption(by processor: TextProcessing, editor: EditorView, at range: NSRange) {
let processors = activeProcessors.filter { $0.name != processor.name }
processors.forEach { $0.processInterrupted(editor: editor, at: range) }
Expand Down
11 changes: 11 additions & 0 deletions Proton/Tests/TextProcessors/Mocks/MockTextProcessor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ class MockTextProcessor: TextProcessing {
var onDidProcess: ((EditorView) -> Void)?
var onShouldProcess: ((EditorView, NSRange, String) -> Bool)?

var willProcessEditing: ((EditorView, NSTextStorage.EditActions, NSRange, Int) -> Void)?
var didProcessEditing: ((EditorView, NSTextStorage.EditActions, NSRange, Int) -> Void)?

var processorCondition: (EditorView, NSRange) -> Bool

init(name: String = "MockTextProcessor", processorCondition: @escaping (EditorView, NSRange) -> Bool = { _, _ in true }) {
Expand Down Expand Up @@ -75,4 +78,12 @@ class MockTextProcessor: TextProcessing {
func shouldProcess(_ editorView: EditorView, shouldProcessTextIn range: NSRange, replacementText text: String) -> Bool {
return onShouldProcess?(editorView, range, text) ?? true
}

func willProcessEditing(editor: EditorView, editedMask: NSTextStorage.EditActions, range editedRange: NSRange, changeInLength delta: Int) {
willProcessEditing?(editor, editedMask, editedRange, delta)
}

func didProcessEditing(editor: EditorView, editedMask: NSTextStorage.EditActions, range editedRange: NSRange, changeInLength delta: Int) {
didProcessEditing?(editor, editedMask, editedRange, delta)
}
}
94 changes: 94 additions & 0 deletions Proton/Tests/TextProcessors/TextProcessorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,100 @@ class TextProcessorTests: XCTestCase {

waitForExpectations(timeout: 1.0)
}

func testInvokesWillProcessEditingContent() {
let testExpectation = functionExpectation()

let editor = EditorView()

let name = "TextProcessorTest"
let mockProcessor = MockTextProcessor(name: name)
mockProcessor.willProcessEditing = { processedEditor, editingMask, range, delta in
XCTAssertEqual(processedEditor, editor)
XCTAssertTrue(editingMask.contains(.editedAttributes))
XCTAssertTrue(editingMask.contains(.editedCharacters))
XCTAssertEqual(range, editor.attributedText.fullRange)
XCTAssertEqual(delta, editor.contentLength)
testExpectation.fulfill()
}
let testString = NSAttributedString(string: "test")
editor.registerProcessor(mockProcessor)
editor.replaceCharacters(in: .zero, with: testString)
waitForExpectations(timeout: 1.0)
}

func testInvokesDidProcessEditingContent() {
let testExpectation = functionExpectation()

let editor = EditorView()

let name = "TextProcessorTest"
let mockProcessor = MockTextProcessor(name: name)
mockProcessor.didProcessEditing = { processedEditor, editingMask, range, delta in
XCTAssertEqual(processedEditor, editor)
XCTAssertTrue(editingMask.contains(.editedAttributes))
XCTAssertTrue(editingMask.contains(.editedCharacters))
XCTAssertEqual(range, editor.attributedText.fullRange)
XCTAssertEqual(delta, editor.contentLength)
testExpectation.fulfill()
}
let testString = NSAttributedString(string: "test")
editor.registerProcessor(mockProcessor)
editor.replaceCharacters(in: .zero, with: testString)
waitForExpectations(timeout: 1.0)
}

func testInvokesWillProcessAttributeChanges() {
let testExpectation = functionExpectation()

let editor = EditorView()

let name = "TextProcessorTest"
let mockProcessor = MockTextProcessor(name: name)

let testString = NSAttributedString(string: "test")
editor.registerProcessor(mockProcessor)
editor.replaceCharacters(in: .zero, with: testString)

let processRange = NSRange(location: 2, length: 2)
mockProcessor.willProcessEditing = { processedEditor, editingMask, range, delta in
XCTAssertTrue(editingMask.contains(.editedAttributes))
XCTAssertFalse(editingMask.contains(.editedCharacters))
XCTAssertEqual(range, processRange)
testExpectation.fulfill()
}

editor.selectedRange = processRange
BoldCommand().execute(on: editor)

waitForExpectations(timeout: 1.0)
}

func testInvokesDidlProcessAttributeChanges() {
let testExpectation = functionExpectation()

let editor = EditorView()

let name = "TextProcessorTest"
let mockProcessor = MockTextProcessor(name: name)

let testString = NSAttributedString(string: "test")
editor.registerProcessor(mockProcessor)
editor.replaceCharacters(in: .zero, with: testString)

let processRange = NSRange(location: 2, length: 2)
mockProcessor.didProcessEditing = { processedEditor, editingMask, range, delta in
XCTAssertTrue(editingMask.contains(.editedAttributes))
XCTAssertFalse(editingMask.contains(.editedCharacters))
XCTAssertEqual(range, processRange)
testExpectation.fulfill()
}

editor.selectedRange = processRange
BoldCommand().execute(on: editor)

waitForExpectations(timeout: 1.0)
}
}

extension EditorView {
Expand Down

0 comments on commit 3e0252f

Please sign in to comment.