Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added additional delegate callbacks on TextProcessing to inform consum… #326

Merged
merged 3 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading