Fix message completion trigger to work anywhere in the message (#3696)

* Fix message completion trigger to work anywhere in the message

* Add tests for display suggestion in difference message positions

* Fix suggestion trigger could support multiple mention symbol

* Remove optional parameter type and tweak argument name to hide

* Optimize the suggestions trigger using regex

* Fix the cursor at wrong position after selected suggestion

* Modify the rawSuggestionText pass data in right way

* Modify mention symbol to use regex type

* Update ElementX/Sources/Screens/RoomScreen/ComposerToolbar/CompletionSuggestionService.swift

Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com>

* Update generated mock

---------

Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com>
This commit is contained in:
Vickcoo
2025-02-13 23:13:09 +08:00
committed by GitHub
parent 8ee1427809
commit 4b43d901f8
14 changed files with 151 additions and 59 deletions
@@ -8,8 +8,8 @@
import Combine
import Foundation
private enum SuggestionTriggerPattern: Character {
case at = "@"
private enum SuggestionTriggerRegex {
static let at = /@\w+/
}
final class CompletionSuggestionService: CompletionSuggestionServiceProtocol {
@@ -42,7 +42,8 @@ final class CompletionSuggestionService: CompletionSuggestionServiceProtocol {
return SuggestionItem.user(item: .init(id: member.userID,
displayName: member.displayName,
avatarURL: member.avatarURL,
range: suggestionTrigger.range))
range: suggestionTrigger.range,
rawSuggestionText: suggestionTrigger.text))
}
if self.canMentionAllUsers,
@@ -52,7 +53,8 @@ final class CompletionSuggestionService: CompletionSuggestionServiceProtocol {
.insert(SuggestionItem.allUsers(item: .init(id: PillConstants.atRoom,
displayName: PillConstants.everyone,
avatarURL: self.roomProxy.infoPublisher.value.avatarURL,
range: suggestionTrigger.range)), at: 0)
range: suggestionTrigger.range,
rawSuggestionText: suggestionTrigger.text)), at: 0)
}
return membersSuggestion
@@ -73,8 +75,8 @@ final class CompletionSuggestionService: CompletionSuggestionServiceProtocol {
}
}
func processTextMessage(_ textMessage: String?) {
setSuggestionTrigger(detectTriggerInText(textMessage))
func processTextMessage(_ textMessage: String, selectedRange: NSRange) {
setSuggestionTrigger(detectTriggerInText(textMessage, selectedRange: selectedRange))
}
func setSuggestionTrigger(_ suggestionTrigger: SuggestionTrigger?) {
@@ -83,23 +85,25 @@ final class CompletionSuggestionService: CompletionSuggestionServiceProtocol {
// MARK: - Private
private func detectTriggerInText(_ text: String?) -> SuggestionTrigger? {
guard let text else {
private func detectTriggerInText(_ text: String, selectedRange: NSRange) -> SuggestionTrigger? {
let matches = text.matches(of: SuggestionTriggerRegex.at)
let match = matches
.first { matchResult in
let lowerBound = matchResult.range.lowerBound.utf16Offset(in: matchResult.base)
let upperBound = matchResult.range.upperBound.utf16Offset(in: matchResult.base)
return selectedRange.location >= lowerBound
&& selectedRange.location <= upperBound
&& selectedRange.length <= upperBound - lowerBound
}
guard let match else {
return nil
}
let components = text.components(separatedBy: .whitespaces)
var suggestionText = String(text[match.range])
suggestionText.removeFirst()
guard var lastComponent = components.last,
let range = text.range(of: lastComponent, options: .backwards),
lastComponent.count > 0,
let suggestionKey = SuggestionTriggerPattern(rawValue: lastComponent.removeFirst()),
// If a second character exists and is the same as the key it shouldn't trigger.
lastComponent.first != suggestionKey.rawValue else {
return nil
}
return .init(type: .user, text: lastComponent, range: NSRange(range, in: text))
return .init(type: .user, text: suggestionText, range: NSRange(match.range, in: text))
}
private static func shouldIncludeMember(userID: String, displayName: String?, searchText: String) -> Bool {