From 22386a42b8062e1be2a74675c1eb321eae151ec8 Mon Sep 17 00:00:00 2001 From: Taksh Date: Wed, 29 Jul 2026 11:43:15 +0300 Subject: [PATCH] fix: make composer autocorrect token-aware Enable autocorrect for prose, but disable it while the current token starts with /, @, or # so commands, mentions, and channels stay exact and aren't fed into keyboard learning. --- bitchat/Utils/ComposerAutocorrect.swift | 45 +++++++++++++++++++++ bitchat/Views/ContentComposerView.swift | 14 +++++-- bitchatTests/ComposerAutocorrectTests.swift | 33 +++++++++++++++ 3 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 bitchat/Utils/ComposerAutocorrect.swift create mode 100644 bitchatTests/ComposerAutocorrectTests.swift diff --git a/bitchat/Utils/ComposerAutocorrect.swift b/bitchat/Utils/ComposerAutocorrect.swift new file mode 100644 index 00000000..8a20d0b7 --- /dev/null +++ b/bitchat/Utils/ComposerAutocorrect.swift @@ -0,0 +1,45 @@ +// +// ComposerAutocorrect.swift +// bitchat +// +// This is free and unencumbered software released into the public domain. +// For more information, see +// + +import Foundation + +/// Token-aware autocorrect policy for the message composer (#969). +/// +/// Autocorrect helps with prose but fights `/commands`, `@mentions`, and +/// `#channels`. Disable it while the token under the cursor starts with one +/// of those sigils; leave it on otherwise. The composer TextField only exposes +/// the text (not a live selection), so callers pass the caret — typically the +/// end of the string, matching autocomplete. +enum ComposerAutocorrect { + /// Sigils that mean "exact token, don't rewrite me". + static let specialPrefixes: Set = ["/", "@", "#"] + + /// Whether `.autocorrectionDisabled` should be on for this caret position. + static func shouldDisable(for text: String, cursorPosition: Int) -> Bool { + let token = currentToken(in: text, cursorPosition: cursorPosition) + guard let first = token.first else { return false } + return specialPrefixes.contains(first) + } + + /// The whitespace-delimited token containing `cursorPosition` (or ending + /// at it when the caret sits on a boundary). + static func currentToken(in text: String, cursorPosition: Int) -> String { + guard !text.isEmpty else { return "" } + let clamped = min(max(0, cursorPosition), text.count) + let end = text.index(text.startIndex, offsetBy: clamped) + let before = text[..