diff --git a/bitchat/Services/AutocompleteService.swift b/bitchat/Services/AutocompleteService.swift index 6c55f34e..3e739e74 100644 --- a/bitchat/Services/AutocompleteService.swift +++ b/bitchat/Services/AutocompleteService.swift @@ -2,7 +2,7 @@ // AutocompleteService.swift // bitchat // -// Handles autocomplete suggestions for mentions and commands +// Handles autocomplete suggestions for mentions // This is free and unencumbered software released into the public domain. // @@ -10,49 +10,47 @@ import Foundation /// Manages autocomplete functionality for chat final class AutocompleteService { - private let mentionRegex = try? NSRegularExpression(pattern: "@([\\p{L}0-9_]*)$", options: []) + private typealias Patterns = MessageFormattingEngine.Patterns /// Get autocomplete suggestions for current text func getSuggestions(for text: String, peers: [String], cursorPosition: Int) -> (suggestions: [String], range: NSRange?) { let textToPosition = String(text.prefix(cursorPosition)) - - // Check for mention autocomplete + + // Mentions only — slash-command suggestions are owned by + // `CommandSuggestionsView` / the composer, not this service. if let (mentionSuggestions, mentionRange) = getMentionSuggestions(textToPosition, peers: peers) { return (mentionSuggestions, mentionRange) } - - // Don't handle command autocomplete here - ContentView handles it with better UI - // if let (commandSuggestions, commandRange) = getCommandSuggestions(textToPosition) { - // return (commandSuggestions, commandRange) - // } - + return ([], nil) } - + /// Apply selected suggestion to text func applySuggestion(_ suggestion: String, to text: String, range: NSRange) -> String { guard let textRange = Range(range, in: text) else { return text } - + var replacement = suggestion - + // Add space after command if it takes arguments if suggestion.hasPrefix("/") && needsArgument(command: suggestion) { replacement += " " } - + return text.replacingCharacters(in: textRange, with: replacement) } - + // MARK: - Private Methods - + private func getMentionSuggestions(_ text: String, peers: [String]) -> ([String], NSRange)? { - guard let regex = mentionRegex else { return nil } - let nsText = text as NSString - let matches = regex.matches(in: text, options: [], range: NSRange(location: 0, length: nsText.length)) - + let matches = Patterns.mentionAutocomplete.matches( + in: text, + options: [], + range: NSRange(location: 0, length: nsText.length) + ) + guard let match = matches.last else { return nil } - + let fullRange = match.range(at: 0) let captureRange = match.range(at: 1) let prefix = nsText.substring(with: captureRange).normalizedNickname.lowercased() @@ -62,10 +60,10 @@ final class AutocompleteService { .sorted() .prefix(5) .map { "@\($0)" } - + return suggestions.isEmpty ? nil : (Array(suggestions), fullRange) } - + private func needsArgument(command: String) -> Bool { switch command { case "/who", "/clear": diff --git a/bitchat/Services/MessageFormattingEngine.swift b/bitchat/Services/MessageFormattingEngine.swift index 88a32e3c..1578f16b 100644 --- a/bitchat/Services/MessageFormattingEngine.swift +++ b/bitchat/Services/MessageFormattingEngine.swift @@ -43,6 +43,9 @@ final class MessageFormattingEngine { static let mention = SafeRegex.compile("@([\\p{L}0-9_]+(?:#[a-fA-F0-9]{4})?)") + /// Trailing partial `@nickname` at the composer cursor — autocomplete only. + static let mentionAutocomplete = SafeRegex.compile("@([\\p{L}0-9_]*)$") + static let cashu = SafeRegex.compile("\\bcashu[AB][A-Za-z0-9._-]{40,}\\b") static let bolt11 = SafeRegex.compile("(?i)\\bln(bc|tb|bcrt)[0-9][a-z0-9]{50,}\\b")