From 802072ca933dfa414ad8f0fe0c95ce99e72dea37 Mon Sep 17 00:00:00 2001 From: Bruno Canepa <8711973+bruncanepa@users.noreply.github.com> Date: Fri, 3 Jul 2026 12:53:58 -0300 Subject: [PATCH] Allow empty custom-dictionary replacements to delete text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Users can now map a trigger to an empty replacement to remove it from the transcript (e.g. the Spanish inverted marks "¿"/"¡"). Two things blocked this: - Validation in the Manual Add composer and the Edit sheet required a non-empty replacement. Both now only require at least one trigger. - More subtly, rebuildDictionaryCache wrapped every trigger in `\b…\b`. ICU word boundaries never match around punctuation, so a punctuation-only trigger like "¿" matched nothing even once empty replacements were allowed. Boundaries are now applied conditionally, only where the trigger edge is a word character, so punctuation triggers match while word triggers keep their existing behavior. Co-Authored-By: Claude Opus 4.8 --- Sources/Fluid/Services/ASRService.swift | 13 ++++++++++++- Sources/Fluid/UI/CustomDictionaryView.swift | 4 ++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/Sources/Fluid/Services/ASRService.swift b/Sources/Fluid/Services/ASRService.swift index f8a244fa..db06f8ee 100644 --- a/Sources/Fluid/Services/ASRService.swift +++ b/Sources/Fluid/Services/ASRService.swift @@ -2982,8 +2982,13 @@ final class ASRService: ObservableObject { guard !trigger.isEmpty else { continue } let escapedTrigger = NSRegularExpression.escapedPattern(for: trigger) + // Only apply `\b` word boundaries where the trigger edge is a word + // character. Punctuation-only triggers (e.g. "¿") have no adjacent + // word boundary, so `\b…\b` would never match them. + let leadingBoundary = (trigger.first?.isWordCharacter ?? false) ? "\\b" : "" + let trailingBoundary = (trigger.last?.isWordCharacter ?? false) ? "\\b" : "" guard let regex = try? NSRegularExpression( - pattern: "\\b" + escapedTrigger + "\\b", + pattern: leadingBoundary + escapedTrigger + trailingBoundary, options: .caseInsensitive ) else { continue } @@ -3131,6 +3136,12 @@ private extension Character { return false } } + + /// Matches the regex `\w` class (letters, digits, or underscore) so we only + /// wrap dictionary triggers in `\b` boundaries where they actually apply. + var isWordCharacter: Bool { + self == "_" || self.isLetter || self.isNumber + } } // swiftlint:enable type_body_length diff --git a/Sources/Fluid/UI/CustomDictionaryView.swift b/Sources/Fluid/UI/CustomDictionaryView.swift index c1c37a2e..8de52864 100644 --- a/Sources/Fluid/UI/CustomDictionaryView.swift +++ b/Sources/Fluid/UI/CustomDictionaryView.swift @@ -192,8 +192,8 @@ struct CustomDictionaryView: View { } private var canAddManualReplacement: Bool { + // An empty replacement is allowed: it deletes the trigger from the transcript. !self.manualTriggers.isEmpty && - !self.manualReplacement.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && self.manualDuplicateTriggers.isEmpty } @@ -2221,8 +2221,8 @@ struct EditDictionaryEntrySheet: View { } private var canSave: Bool { + // An empty replacement is allowed: it deletes the trigger from the transcript. !self.parseTriggers().isEmpty && - !self.replacement.trimmingCharacters(in: .whitespaces).isEmpty && self.duplicateTriggers.isEmpty }