Fix text editing issues on characters added right after mention link
Changelog-Fixed: Fix text editing issues on characters added right after mention link Closes: https://github.com/damus-io/damus/issues/1375 Signed-off-by: Daniel D’Aquino <daniel@daquino.me> Tested-by: William Casarin <jb55@jb55.com> Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
committed by
William Casarin
parent
c464a26151
commit
dcb94635ea
@@ -383,6 +383,7 @@
|
|||||||
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; };
|
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; };
|
||||||
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; };
|
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; };
|
||||||
D2277EEA2A089BD5006C3807 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2277EE92A089BD5006C3807 /* Router.swift */; };
|
D2277EEA2A089BD5006C3807 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2277EE92A089BD5006C3807 /* Router.swift */; };
|
||||||
|
D71DC1EC2A9129C3006E207C /* PostViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71DC1EB2A9129C3006E207C /* PostViewTests.swift */; };
|
||||||
D78525252A7B2EA4002FA637 /* NoteContentViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */; };
|
D78525252A7B2EA4002FA637 /* NoteContentViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */; };
|
||||||
D7DEEF2F2A8C021E00E0C99F /* NostrEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */; };
|
D7DEEF2F2A8C021E00E0C99F /* NostrEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */; };
|
||||||
E4FA1C032A24BB7F00482697 /* SearchSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */; };
|
E4FA1C032A24BB7F00482697 /* SearchSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */; };
|
||||||
@@ -936,6 +937,7 @@
|
|||||||
BA693073295D649800ADDB87 /* UserSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsStore.swift; sourceTree = "<group>"; };
|
BA693073295D649800ADDB87 /* UserSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsStore.swift; sourceTree = "<group>"; };
|
||||||
BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectWalletView.swift; sourceTree = "<group>"; };
|
BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectWalletView.swift; sourceTree = "<group>"; };
|
||||||
D2277EE92A089BD5006C3807 /* Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = "<group>"; };
|
D2277EE92A089BD5006C3807 /* Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = "<group>"; };
|
||||||
|
D71DC1EB2A9129C3006E207C /* PostViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostViewTests.swift; sourceTree = "<group>"; };
|
||||||
D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteContentViewTests.swift; sourceTree = "<group>"; };
|
D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteContentViewTests.swift; sourceTree = "<group>"; };
|
||||||
D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrEventTests.swift; sourceTree = "<group>"; };
|
D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrEventTests.swift; sourceTree = "<group>"; };
|
||||||
E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSettingsView.swift; sourceTree = "<group>"; };
|
E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSettingsView.swift; sourceTree = "<group>"; };
|
||||||
@@ -1751,6 +1753,7 @@
|
|||||||
4C684A542A7E91FE005E6031 /* LongPostTests.swift */,
|
4C684A542A7E91FE005E6031 /* LongPostTests.swift */,
|
||||||
4C684A562A7FFAE6005E6031 /* UrlTests.swift */,
|
4C684A562A7FFAE6005E6031 /* UrlTests.swift */,
|
||||||
D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */,
|
D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */,
|
||||||
|
D71DC1EB2A9129C3006E207C /* PostViewTests.swift */,
|
||||||
);
|
);
|
||||||
path = damusTests;
|
path = damusTests;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -2457,6 +2460,7 @@
|
|||||||
4C9B0DEE2A65A75F00CBDA21 /* AttrStringTestExtensions.swift in Sources */,
|
4C9B0DEE2A65A75F00CBDA21 /* AttrStringTestExtensions.swift in Sources */,
|
||||||
4C19AE552A5D977400C90DB7 /* HashtagTests.swift in Sources */,
|
4C19AE552A5D977400C90DB7 /* HashtagTests.swift in Sources */,
|
||||||
3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */,
|
3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */,
|
||||||
|
D71DC1EC2A9129C3006E207C /* PostViewTests.swift in Sources */,
|
||||||
3AAC7A022A60FE72002B50DF /* LocalizationUtilTests.swift in Sources */,
|
3AAC7A022A60FE72002B50DF /* LocalizationUtilTests.swift in Sources */,
|
||||||
D7DEEF2F2A8C021E00E0C99F /* NostrEventTests.swift in Sources */,
|
D7DEEF2F2A8C021E00E0C99F /* NostrEventTests.swift in Sources */,
|
||||||
4C8D00D429E3C5D40036AF10 /* NIP19Tests.swift in Sources */,
|
4C8D00D429E3C5D40036AF10 /* NIP19Tests.swift in Sources */,
|
||||||
|
|||||||
@@ -204,6 +204,8 @@ struct PostView: View {
|
|||||||
TextViewWrapper(attributedText: $post, postTextViewCanScroll: $postTextViewCanScroll, cursorIndex: newCursorIndex, getFocusWordForMention: { word, range in
|
TextViewWrapper(attributedText: $post, postTextViewCanScroll: $postTextViewCanScroll, cursorIndex: newCursorIndex, getFocusWordForMention: { word, range in
|
||||||
focusWordAttributes = (word, range)
|
focusWordAttributes = (word, range)
|
||||||
self.newCursorIndex = nil
|
self.newCursorIndex = nil
|
||||||
|
}, updateCursorPosition: { newCursorIndex in
|
||||||
|
self.newCursorIndex = newCursorIndex
|
||||||
})
|
})
|
||||||
.environmentObject(tagModel)
|
.environmentObject(tagModel)
|
||||||
.focused($focus)
|
.focused($focus)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ struct TextViewWrapper: UIViewRepresentable {
|
|||||||
|
|
||||||
let cursorIndex: Int?
|
let cursorIndex: Int?
|
||||||
var getFocusWordForMention: ((String?, NSRange?) -> Void)? = nil
|
var getFocusWordForMention: ((String?, NSRange?) -> Void)? = nil
|
||||||
|
let updateCursorPosition: ((Int) -> Void)
|
||||||
|
|
||||||
func makeUIView(context: Context) -> UITextView {
|
func makeUIView(context: Context) -> UITextView {
|
||||||
let textView = UITextView()
|
let textView = UITextView()
|
||||||
@@ -34,11 +35,11 @@ struct TextViewWrapper: UIViewRepresentable {
|
|||||||
|
|
||||||
func updateUIView(_ uiView: UITextView, context: Context) {
|
func updateUIView(_ uiView: UITextView, context: Context) {
|
||||||
uiView.isScrollEnabled = postTextViewCanScroll
|
uiView.isScrollEnabled = postTextViewCanScroll
|
||||||
let range = uiView.selectedRange
|
|
||||||
uiView.attributedText = attributedText
|
uiView.attributedText = attributedText
|
||||||
|
|
||||||
TextViewWrapper.setTextProperties(uiView)
|
TextViewWrapper.setTextProperties(uiView)
|
||||||
setCursorPosition(textView: uiView)
|
setCursorPosition(textView: uiView)
|
||||||
|
let range = uiView.selectedRange
|
||||||
|
|
||||||
uiView.selectedRange = NSRange(location: range.location + tagModel.diff, length: range.length)
|
uiView.selectedRange = NSRange(location: range.location + tagModel.diff, length: range.length)
|
||||||
tagModel.diff = 0
|
tagModel.diff = 0
|
||||||
@@ -52,16 +53,18 @@ struct TextViewWrapper: UIViewRepresentable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func makeCoordinator() -> Coordinator {
|
func makeCoordinator() -> Coordinator {
|
||||||
Coordinator(attributedText: $attributedText, getFocusWordForMention: getFocusWordForMention)
|
Coordinator(attributedText: $attributedText, getFocusWordForMention: getFocusWordForMention, updateCursorPosition: updateCursorPosition)
|
||||||
}
|
}
|
||||||
|
|
||||||
class Coordinator: NSObject, UITextViewDelegate {
|
class Coordinator: NSObject, UITextViewDelegate {
|
||||||
@Binding var attributedText: NSMutableAttributedString
|
@Binding var attributedText: NSMutableAttributedString
|
||||||
var getFocusWordForMention: ((String?, NSRange?) -> Void)? = nil
|
var getFocusWordForMention: ((String?, NSRange?) -> Void)? = nil
|
||||||
|
let updateCursorPosition: ((Int) -> Void)
|
||||||
|
|
||||||
init(attributedText: Binding<NSMutableAttributedString>, getFocusWordForMention: ((String?, NSRange?) -> Void)?) {
|
init(attributedText: Binding<NSMutableAttributedString>, getFocusWordForMention: ((String?, NSRange?) -> Void)?, updateCursorPosition: @escaping ((Int) -> Void)) {
|
||||||
_attributedText = attributedText
|
_attributedText = attributedText
|
||||||
self.getFocusWordForMention = getFocusWordForMention
|
self.getFocusWordForMention = getFocusWordForMention
|
||||||
|
self.updateCursorPosition = updateCursorPosition
|
||||||
}
|
}
|
||||||
|
|
||||||
func textViewDidChange(_ textView: UITextView) {
|
func textViewDidChange(_ textView: UITextView) {
|
||||||
@@ -96,6 +99,65 @@ struct TextViewWrapper: UIViewRepresentable {
|
|||||||
}
|
}
|
||||||
return NSRange(location: startOffset, length: length)
|
return NSRange(location: startOffset, length: length)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This `UITextViewDelegate` method is automatically called by the editor when edits occur, to check whether a change should occur
|
||||||
|
// We will use this method to manually handle edits concerning mention ("@") links, to avoid manual text edits to attributed mention links
|
||||||
|
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
||||||
|
guard let attributedString = textView.attributedText else {
|
||||||
|
return true // If we cannot get an attributed string, just fail gracefully and allow changes
|
||||||
|
}
|
||||||
|
var mutable = NSMutableAttributedString(attributedString: attributedString)
|
||||||
|
|
||||||
|
let entireRange = NSRange(location: 0, length: attributedString.length)
|
||||||
|
var shouldAllowChange = true
|
||||||
|
var performEditActionManually = false
|
||||||
|
|
||||||
|
attributedString.enumerateAttribute(.link, in: entireRange, options: []) { (value, linkRange, stop) in
|
||||||
|
guard value != nil else {
|
||||||
|
return // This range is not a link. Skip checking.
|
||||||
|
}
|
||||||
|
|
||||||
|
if range.contains(linkRange.upperBound) && range.contains(linkRange.lowerBound) {
|
||||||
|
// Edit range engulfs all of this link's range.
|
||||||
|
// This link will naturally disappear, so no work needs to be done in this range.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
else if linkRange.intersection(range) != nil {
|
||||||
|
// If user tries to change an existing link directly, remove the link attribute
|
||||||
|
mutable.removeAttribute(.link, range: linkRange)
|
||||||
|
// Perform action manually to flush above changes to the view, and to prevent the character being added from having an attributed link property
|
||||||
|
performEditActionManually = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
else if range.location == linkRange.location + linkRange.length && range.length == 0 {
|
||||||
|
// If we are inserting a character at the right edge of a link, UITextInput tends to include the new character inside the link.
|
||||||
|
// Therefore, we need to manually append that character outside of the link
|
||||||
|
performEditActionManually = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if performEditActionManually {
|
||||||
|
shouldAllowChange = false
|
||||||
|
addUnattributedText(text, to: &mutable, inRange: range)
|
||||||
|
attributedText = mutable
|
||||||
|
|
||||||
|
// Move caret to the end of the newly changed text.
|
||||||
|
updateCursorPosition(range.location + text.count)
|
||||||
|
}
|
||||||
|
|
||||||
|
return shouldAllowChange
|
||||||
|
}
|
||||||
|
|
||||||
|
func addUnattributedText(_ text: String, to attributedString: inout NSMutableAttributedString, inRange range: NSRange) {
|
||||||
|
if range.length == 0 {
|
||||||
|
attributedString.insert(NSAttributedString(string: text, attributes: nil), at: range.location)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
attributedString.replaceCharacters(in: range, with: text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
143
damusTests/PostViewTests.swift
Normal file
143
damusTests/PostViewTests.swift
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
//
|
||||||
|
// PostViewTests.swift
|
||||||
|
// damusTests
|
||||||
|
//
|
||||||
|
// Created by Daniel D’Aquino on 2023-08-19.
|
||||||
|
//
|
||||||
|
import Foundation
|
||||||
|
import XCTest
|
||||||
|
@testable import damus
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
final class PostViewTests: XCTestCase {
|
||||||
|
|
||||||
|
override func setUpWithError() throws {
|
||||||
|
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tearDownWithError() throws {
|
||||||
|
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Based on https://github.com/damus-io/damus/issues/1375
|
||||||
|
/// Tests whether the editor properly handles mention links after they have been added, to avoid manual editing of attributed links
|
||||||
|
func testMentionLinkEditorHandling() throws {
|
||||||
|
var content: NSMutableAttributedString
|
||||||
|
|
||||||
|
// Test normal insertion
|
||||||
|
checkMentionLinkEditorHandling(content: NSMutableAttributedString(string: "Hello"), replacementText: "@", replacementRange: NSRange(location: 0, length: 0), shouldBeAbleToChangeAutomatically: true)
|
||||||
|
checkMentionLinkEditorHandling(content: NSMutableAttributedString(string: "Hello "), replacementText: "@", replacementRange: NSRange(location: 6, length: 0), shouldBeAbleToChangeAutomatically: true)
|
||||||
|
checkMentionLinkEditorHandling(content: NSMutableAttributedString(string: "Helo "), replacementText: "l", replacementRange: NSRange(location: 3, length: 0), shouldBeAbleToChangeAutomatically: true)
|
||||||
|
|
||||||
|
// Test normal backspacing
|
||||||
|
checkMentionLinkEditorHandling(content: NSMutableAttributedString(string: "Hello"), replacementText: "", replacementRange: NSRange(location: 5, length: 1), shouldBeAbleToChangeAutomatically: true)
|
||||||
|
checkMentionLinkEditorHandling(content: NSMutableAttributedString(string: "Hello "), replacementText: "", replacementRange: NSRange(location: 6, length: 1), shouldBeAbleToChangeAutomatically: true)
|
||||||
|
checkMentionLinkEditorHandling(content: NSMutableAttributedString(string: "Helo "), replacementText: "", replacementRange: NSRange(location: 3, length: 1), shouldBeAbleToChangeAutomatically: true)
|
||||||
|
|
||||||
|
// Test normal insertion after mention link
|
||||||
|
content = NSMutableAttributedString(string: "Hello @user ")
|
||||||
|
content.addAttribute(.link, value: "damus:1234", range: NSRange(location: 6, length: 5))
|
||||||
|
checkMentionLinkEditorHandling(content: content, replacementText: "a", replacementRange: NSRange(location: 12, length: 0), shouldBeAbleToChangeAutomatically: true)
|
||||||
|
|
||||||
|
// Test insertion right at the end of a mention link, at the end of the text
|
||||||
|
content = NSMutableAttributedString(string: "Hello @user")
|
||||||
|
content.addAttribute(.link, value: "damus:1234", range: NSRange(location: 6, length: 5))
|
||||||
|
checkMentionLinkEditorHandling(content: content, replacementText: ",", replacementRange: NSRange(location: 11, length: 0), shouldBeAbleToChangeAutomatically: false, expectedNewCursorIndex: 12, handleNewContent: { newManuallyEditedContent in
|
||||||
|
XCTAssertEqual(newManuallyEditedContent.string, "Hello @user,")
|
||||||
|
XCTAssertNil(newManuallyEditedContent.attribute(.link, at: 11, effectiveRange: nil))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test insertion right at the end of a mention link, in the middle of the text
|
||||||
|
content = NSMutableAttributedString(string: "Hello @user how are you?")
|
||||||
|
content.addAttribute(.link, value: "damus:1234", range: NSRange(location: 6, length: 5))
|
||||||
|
checkMentionLinkEditorHandling(content: content, replacementText: ",", replacementRange: NSRange(location: 11, length: 0), shouldBeAbleToChangeAutomatically: false, expectedNewCursorIndex: 12, handleNewContent: { newManuallyEditedContent in
|
||||||
|
XCTAssertEqual(newManuallyEditedContent.string, "Hello @user, how are you?")
|
||||||
|
XCTAssertNil(newManuallyEditedContent.attribute(.link, at: 11, effectiveRange: nil))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test insertion in the middle of a mention link to check if the link is removed
|
||||||
|
content = NSMutableAttributedString(string: "Hello @user how are you?")
|
||||||
|
content.addAttribute(.link, value: "damus:1234", range: NSRange(location: 6, length: 5))
|
||||||
|
checkMentionLinkEditorHandling(content: content, replacementText: "a", replacementRange: NSRange(location: 8, length: 0), shouldBeAbleToChangeAutomatically: false, expectedNewCursorIndex: 9, handleNewContent: { newManuallyEditedContent in
|
||||||
|
XCTAssertEqual(newManuallyEditedContent.string, "Hello @uaser how are you?")
|
||||||
|
XCTAssertNil(newManuallyEditedContent.attribute(.link, at: 8, effectiveRange: nil))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test insertion in the middle of a mention link to check if the link is removed, at the end of the text
|
||||||
|
content = NSMutableAttributedString(string: "Hello @user")
|
||||||
|
content.addAttribute(.link, value: "damus:1234", range: NSRange(location: 6, length: 5))
|
||||||
|
checkMentionLinkEditorHandling(content: content, replacementText: "a", replacementRange: NSRange(location: 8, length: 0), shouldBeAbleToChangeAutomatically: false, expectedNewCursorIndex: 9, handleNewContent: { newManuallyEditedContent in
|
||||||
|
XCTAssertEqual(newManuallyEditedContent.string, "Hello @uaser")
|
||||||
|
XCTAssertNil(newManuallyEditedContent.attribute(.link, at: 8, effectiveRange: nil))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test backspacing right at the end of a mention link, at the end of the text
|
||||||
|
content = NSMutableAttributedString(string: "Hello @user")
|
||||||
|
content.addAttribute(.link, value: "damus:1234", range: NSRange(location: 6, length: 5))
|
||||||
|
checkMentionLinkEditorHandling(content: content, replacementText: "", replacementRange: NSRange(location: 10, length: 1), shouldBeAbleToChangeAutomatically: false, expectedNewCursorIndex: 10, handleNewContent: { newManuallyEditedContent in
|
||||||
|
XCTAssertEqual(newManuallyEditedContent.string, "Hello @use")
|
||||||
|
XCTAssertNil(newManuallyEditedContent.attribute(.link, at: 6, effectiveRange: nil))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test adding text right at the start of a mention link, to check that the link is removed
|
||||||
|
content = NSMutableAttributedString(string: "Hello @user")
|
||||||
|
content.addAttribute(.link, value: "damus:1234", range: NSRange(location: 6, length: 5))
|
||||||
|
checkMentionLinkEditorHandling(content: content, replacementText: "a", replacementRange: NSRange(location: 6, length: 0), shouldBeAbleToChangeAutomatically: false, expectedNewCursorIndex: 7, handleNewContent: { newManuallyEditedContent in
|
||||||
|
XCTAssertEqual(newManuallyEditedContent.string, "Hello a@user")
|
||||||
|
XCTAssertNil(newManuallyEditedContent.attribute(.link, at: 7, effectiveRange: nil))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test that removing one link does not affect the other
|
||||||
|
content = NSMutableAttributedString(string: "Hello @user1 @user2")
|
||||||
|
content.addAttribute(.link, value: "damus:1234", range: NSRange(location: 6, length: 6))
|
||||||
|
content.addAttribute(.link, value: "damus:5678", range: NSRange(location: 13, length: 6))
|
||||||
|
checkMentionLinkEditorHandling(content: content, replacementText: "", replacementRange: NSRange(location: 18, length: 1), shouldBeAbleToChangeAutomatically: false, expectedNewCursorIndex: 18, handleNewContent: { newManuallyEditedContent in
|
||||||
|
XCTAssertEqual(newManuallyEditedContent.string, "Hello @user1 @user")
|
||||||
|
XCTAssertNil(newManuallyEditedContent.attribute(.link, at: 13, effectiveRange: nil))
|
||||||
|
XCTAssertNotNil(newManuallyEditedContent.attribute(.link, at: 6, effectiveRange: nil))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test that replacing a whole range intersecting with two links removes both links
|
||||||
|
content = NSMutableAttributedString(string: "Hello @user1 @user2")
|
||||||
|
content.addAttribute(.link, value: "damus:1234", range: NSRange(location: 6, length: 6))
|
||||||
|
content.addAttribute(.link, value: "damus:5678", range: NSRange(location: 13, length: 6))
|
||||||
|
checkMentionLinkEditorHandling(content: content, replacementText: "a", replacementRange: NSRange(location: 10, length: 4), shouldBeAbleToChangeAutomatically: false, expectedNewCursorIndex: 11, handleNewContent: { newManuallyEditedContent in
|
||||||
|
XCTAssertEqual(newManuallyEditedContent.string, "Hello @useauser2")
|
||||||
|
XCTAssertNil(newManuallyEditedContent.attribute(.link, at: 6, effectiveRange: nil))
|
||||||
|
XCTAssertNil(newManuallyEditedContent.attribute(.link, at: 11, effectiveRange: nil))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test that replacing a whole range including two links removes both links naturally
|
||||||
|
content = NSMutableAttributedString(string: "Hello @user1 @user2, how are you?")
|
||||||
|
content.addAttribute(.link, value: "damus:1234", range: NSRange(location: 6, length: 6))
|
||||||
|
content.addAttribute(.link, value: "damus:5678", range: NSRange(location: 13, length: 6))
|
||||||
|
checkMentionLinkEditorHandling(content: content, replacementText: "", replacementRange: NSRange(location: 5, length: 28), shouldBeAbleToChangeAutomatically: true)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkMentionLinkEditorHandling(
|
||||||
|
content: NSMutableAttributedString,
|
||||||
|
replacementText: String,
|
||||||
|
replacementRange: NSRange,
|
||||||
|
shouldBeAbleToChangeAutomatically: Bool,
|
||||||
|
expectedNewCursorIndex: Int? = nil,
|
||||||
|
handleNewContent: ((NSMutableAttributedString) -> Void)? = nil) {
|
||||||
|
let bindingContent: Binding<NSMutableAttributedString> = Binding(get: {
|
||||||
|
return content
|
||||||
|
}, set: { newValue in
|
||||||
|
handleNewContent?(newValue)
|
||||||
|
})
|
||||||
|
let coordinator: TextViewWrapper.Coordinator = TextViewWrapper.Coordinator(attributedText: bindingContent, getFocusWordForMention: nil, updateCursorPosition: { newCursorIndex in
|
||||||
|
if let expectedNewCursorIndex {
|
||||||
|
XCTAssertEqual(newCursorIndex, expectedNewCursorIndex)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
let textView = UITextView()
|
||||||
|
textView.attributedText = content
|
||||||
|
|
||||||
|
XCTAssertEqual(coordinator.textView(textView, shouldChangeTextIn: replacementRange, replacementText: replacementText), shouldBeAbleToChangeAutomatically, "Expected shouldChangeTextIn to return \(shouldBeAbleToChangeAutomatically), but was \(!shouldBeAbleToChangeAutomatically)")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user