From f1fb2168c2f5521be90212cd7d3e3c263308ad83 Mon Sep 17 00:00:00 2001 From: Beingpax Date: Fri, 12 Sep 2025 11:21:00 +0545 Subject: [PATCH] Added support for comma-separated words for word replacement. --- .../Services/WordReplacementService.swift | 11 ++--- .../Dictionary/EditReplacementSheet.swift | 15 ++++--- .../Dictionary/WordReplacementView.swift | 40 ++++++++++++++----- 3 files changed, 46 insertions(+), 20 deletions(-) diff --git a/VoiceInk/Services/WordReplacementService.swift b/VoiceInk/Services/WordReplacementService.swift index 1b84858..79aa210 100644 --- a/VoiceInk/Services/WordReplacementService.swift +++ b/VoiceInk/Services/WordReplacementService.swift @@ -15,12 +15,10 @@ class WordReplacementService { // Apply replacements (case-insensitive) for (original, replacement) in replacements { - let isPhrase = original.contains(" ") || original.trimmingCharacters(in: .whitespacesAndNewlines) != original + let usesBoundaries = usesWordBoundaries(for: original) - if isPhrase || !usesWordBoundaries(for: original) { - modifiedText = modifiedText.replacingOccurrences(of: original, with: replacement, options: .caseInsensitive) - } else { - // Use word boundaries for spaced languages + if usesBoundaries { + // Word-boundary regex for full original string let pattern = "\\b\(NSRegularExpression.escapedPattern(for: original))\\b" if let regex = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive) { let range = NSRange(modifiedText.startIndex..., in: modifiedText) @@ -31,6 +29,9 @@ class WordReplacementService { withTemplate: replacement ) } + } else { + // Fallback substring replace for non-spaced scripts + modifiedText = modifiedText.replacingOccurrences(of: original, with: replacement, options: .caseInsensitive) } } diff --git a/VoiceInk/Views/Dictionary/EditReplacementSheet.swift b/VoiceInk/Views/Dictionary/EditReplacementSheet.swift index 988ffce..50113b2 100644 --- a/VoiceInk/Views/Dictionary/EditReplacementSheet.swift +++ b/VoiceInk/Views/Dictionary/EditReplacementSheet.swift @@ -1,8 +1,5 @@ import SwiftUI - -/// A reusable sheet for editing an existing word replacement entry. -/// Mirrors the UI of `AddReplacementSheet` for consistency while pre-populating -/// the fields with the existing values. +// Edit existing word replacement entry struct EditReplacementSheet: View { @ObservedObject var manager: WordReplacementManager let originalKey: String @@ -84,8 +81,9 @@ struct EditReplacementSheet: View { .font(.caption) .foregroundColor(.secondary) } - TextField("Enter word or phrase to replace", text: $originalWord) + TextField("Enter word or phrase to replace (use commas for multiple)", text: $originalWord) .textFieldStyle(.roundedBorder) + } .padding(.horizontal) @@ -117,7 +115,12 @@ struct EditReplacementSheet: View { private func saveChanges() { let newOriginal = originalWord.trimmingCharacters(in: .whitespacesAndNewlines) let newReplacement = replacementWord.trimmingCharacters(in: .whitespacesAndNewlines) - guard !newOriginal.isEmpty, !newReplacement.isEmpty else { return } + // Ensure at least one non-empty token + let tokens = newOriginal + .split(separator: ",") + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty } + guard !tokens.isEmpty, !newReplacement.isEmpty else { return } manager.updateReplacement(oldOriginal: originalKey, newOriginal: newOriginal, newReplacement: newReplacement) dismiss() diff --git a/VoiceInk/Views/Dictionary/WordReplacementView.swift b/VoiceInk/Views/Dictionary/WordReplacementView.swift index ee8b4d0..a68ea4f 100644 --- a/VoiceInk/Views/Dictionary/WordReplacementView.swift +++ b/VoiceInk/Views/Dictionary/WordReplacementView.swift @@ -23,7 +23,15 @@ class WordReplacementManager: ObservableObject { } func addReplacement(original: String, replacement: String) { - replacements[original] = replacement + // Support comma-separated originals mapping to the same replacement + let tokens = original + .split(separator: ",") + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty } + guard !tokens.isEmpty else { return } + for token in tokens { + replacements[token] = replacement + } } func removeReplacement(original: String) { @@ -31,12 +39,18 @@ class WordReplacementManager: ObservableObject { } func updateReplacement(oldOriginal: String, newOriginal: String, newReplacement: String) { - // Remove the old key if the original text has changed - if oldOriginal != newOriginal { - replacements.removeValue(forKey: oldOriginal) + // Always remove the old key being edited + replacements.removeValue(forKey: oldOriginal) + + // Add one or more new keys (comma-separated) pointing to the same replacement + let tokens = newOriginal + .split(separator: ",") + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty } + guard !tokens.isEmpty else { return } + for token in tokens { + replacements[token] = newReplacement } - // Update (or insert) the new key/value pair - replacements[newOriginal] = newReplacement } } @@ -142,7 +156,7 @@ struct EmptyStateView: View { Text("No Replacements") .font(.headline) - Text("Add word replacements to automatically replace text during AI enhancement.") + Text("Add word replacements to automatically replace text.") .font(.subheadline) .foregroundColor(.secondary) .multilineTextAlignment(.center) @@ -221,9 +235,12 @@ struct AddReplacementSheet: View { .foregroundColor(.secondary) } - TextField("Enter word or phrase to replace", text: $originalWord) + TextField("Enter word or phrase to replace (use commas for multiple)", text: $originalWord) .textFieldStyle(.roundedBorder) .font(.body) + Text("Separate multiple originals with commas, e.g. Voicing, Voice ink, Voiceing") + .font(.caption) + .foregroundColor(.secondary) } .padding(.horizontal) @@ -297,7 +314,12 @@ struct AddReplacementSheet: View { let original = originalWord let replacement = replacementWord - guard !original.isEmpty && !replacement.isEmpty else { return } + // Validate that at least one non-empty token exists + let tokens = original + .split(separator: ",") + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty } + guard !tokens.isEmpty && !replacement.isEmpty else { return } manager.addReplacement(original: original, replacement: replacement) dismiss()