Skip to content

Commit

Permalink
Added additional delegate callbacks on TextProcessing to inform consu…
Browse files Browse the repository at this point in the history
…m… (#326)

* Added additional delegate callbacks on TextProcesing to inform consumers of changes to tex, attributes or both

* Fixed editmask to include characters only if delta is not 0

* Reverted back PRTextStorage change
  • Loading branch information
rajdeep authored Jul 12, 2024
1 parent 7c73a40 commit cfc73c4
Show file tree
Hide file tree
Showing 4 changed files with 190 additions and 0 deletions.
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) { }
}
25 changes: 25 additions & 0 deletions Proton/Sources/Swift/TextProcessors/TextProcessor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ class TextProcessor: NSObject, NSTextStorageDelegate {
var processed = false
let changedText = textStorage.substring(from: editedRange)

let editedMask = getEditedMask(delta: delta)
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 All @@ -81,13 +86,33 @@ class TextProcessor: NSObject, NSTextStorageDelegate {
editor.editorContextDelegate?.editor(editor, didExecuteProcessors: executedProcessors, at: editedRange)
}

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 {
$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 {
processor.willProcess(editor: editor, deletedText: deletedText, insertedText: insertedText, range: range)
}
}

// The editedMask is computed here as fixing the actual bug in PRTextStorage.replaceCharacter ([self edited:])
// causing incorrect editedMask coming-in in this delegate causes TableViewAttachmentSnapshotTests.testRendersTableViewAttachmentInViewportRotation
// to hang, possibly due to persistent layout invalidations. This can be fixed if cell has foreApplyAttributedText on
// which ensures TextStorage to always be consistent state. However, given that there is some unknown, the proper fix
// in PRTextStorage will be added at a later time. It may include dropping need for forceApplyAttributedText.
private func getEditedMask(delta: Int) -> NSTextStorage.EditActions {
guard delta != 0 else {
return .editedAttributes
}
return [.editedCharacters, .editedAttributes]
}

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)
}
}
136 changes: 136 additions & 0 deletions Proton/Tests/TextProcessors/TextProcessorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,142 @@ 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 testInvokesDidProcessEditingContentOnPartialDelete() {
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(delta, -2)
testExpectation.fulfill()
}
let testString = NSAttributedString(string: "test")
editor.replaceCharacters(in: .zero, with: testString)
editor.registerProcessor(mockProcessor)
let processRange = NSRange(location: 2, length: 2)
editor.replaceCharacters(in: processRange, with: NSAttributedString())
waitForExpectations(timeout: 1.0)
}

func testInvokesDidProcessEditingContentOnFullDelete() {
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(delta, -4)
testExpectation.fulfill()
}
let testString = NSAttributedString(string: "test")
editor.replaceCharacters(in: .zero, with: testString)
editor.registerProcessor(mockProcessor)
editor.replaceCharacters(in: editor.attributedText.fullRange, with: NSAttributedString())
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 testInvokesDidProcessAttributeChanges() {
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 cfc73c4

Please sign in to comment.