Pārlūkot izejas kodu

Highlight corrected text in checker results so fixes are visible in content.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 1 mēnesi atpakaļ
vecāks
revīzija
3eedb618f1

+ 18 - 0
gramora/Models/TextCorrection.swift

@@ -0,0 +1,18 @@
+import Foundation
+
+struct TextCorrection: Equatable {
+    let original: String
+    let suggestion: String
+}
+
+extension GrammarIssue {
+    var asTextCorrection: TextCorrection {
+        TextCorrection(original: original, suggestion: suggestion)
+    }
+}
+
+extension SpellingMistake {
+    var asTextCorrection: TextCorrection {
+        TextCorrection(original: original, suggestion: suggestion)
+    }
+}

+ 95 - 0
gramora/Utilities/String+CorrectionHighlighting.swift

@@ -0,0 +1,95 @@
+import SwiftUI
+
+extension String {
+    func attributedStringHighlightingCorrections(
+        in correctedText: String,
+        corrections: [TextCorrection],
+        fontSize: CGFloat = 14
+    ) -> AttributedString {
+        var attributed = AttributedString(correctedText)
+        attributed.font = .system(size: fontSize)
+        attributed.foregroundColor = AppTheme.textPrimary
+
+        guard !corrections.isEmpty else { return attributed }
+
+        let highlightRanges = Self.highlightRanges(
+            originalText: self,
+            correctedText: correctedText,
+            corrections: corrections
+        )
+
+        for range in highlightRanges {
+            guard let attributeRange = Range(range, in: attributed) else { continue }
+            attributed[attributeRange].foregroundColor = AppTheme.teal
+            attributed[attributeRange].font = .system(size: fontSize, weight: .semibold)
+        }
+
+        return attributed
+    }
+
+    private static func highlightRanges(
+        originalText: String,
+        correctedText: String,
+        corrections: [TextCorrection]
+    ) -> [Range<String.Index>] {
+        var result = originalText
+        var ranges: [Range<String.Index>] = []
+
+        let located = corrections.compactMap { correction -> (Range<String.Index>, String)? in
+            guard let range = result.range(of: correction.original) else { return nil }
+            return (range, correction.suggestion)
+        }
+        .sorted { $0.0.lowerBound > $1.0.lowerBound }
+
+        for (range, suggestion) in located {
+            let highlightStart = range.lowerBound
+            guard let highlightEnd = result.index(
+                highlightStart,
+                offsetBy: suggestion.count,
+                limitedBy: result.endIndex
+            ) else { continue }
+
+            result.replaceSubrange(range, with: suggestion)
+            ranges.append(highlightStart..<highlightEnd)
+        }
+
+        if result == correctedText {
+            return ranges
+        }
+
+        return findSuggestionRanges(in: correctedText, corrections: corrections)
+    }
+
+    private static func findSuggestionRanges(
+        in text: String,
+        corrections: [TextCorrection]
+    ) -> [Range<String.Index>] {
+        var usedRanges: [Range<String.Index>] = []
+        var result: [Range<String.Index>] = []
+
+        for correction in corrections where !correction.suggestion.isEmpty {
+            var searchStart = text.startIndex
+
+            while searchStart < text.endIndex {
+                guard let range = text.range(
+                    of: correction.suggestion,
+                    range: searchStart..<text.endIndex
+                ) else { break }
+
+                let overlaps = usedRanges.contains { used in
+                    range.lowerBound < used.upperBound && used.lowerBound < range.upperBound
+                }
+
+                if !overlaps {
+                    usedRanges.append(range)
+                    result.append(range)
+                    break
+                }
+
+                searchStart = text.index(after: range.lowerBound)
+            }
+        }
+
+        return result
+    }
+}

+ 23 - 0
gramora/Views/Components/HighlightedCorrectedText.swift

@@ -0,0 +1,23 @@
+import SwiftUI
+
+struct HighlightedCorrectedText: View {
+    let originalText: String
+    let correctedText: String
+    let corrections: [TextCorrection]
+    var fontSize: CGFloat = 14
+    var padding: CGFloat = 20
+
+    var body: some View {
+        Text(
+            originalText.attributedStringHighlightingCorrections(
+                in: correctedText,
+                corrections: corrections,
+                fontSize: fontSize
+            )
+        )
+        .frame(maxWidth: .infinity, alignment: .topLeading)
+        .multilineTextAlignment(.leading)
+        .textSelection(.enabled)
+        .padding(padding)
+    }
+}

+ 5 - 6
gramora/Views/GrammarCheckerView.swift

@@ -228,12 +228,11 @@ struct GrammarCheckerView: View {
 
             VStack(alignment: .leading, spacing: 0) {
                 ScrollView(.vertical, showsIndicators: true) {
-                    Text(viewModel.correctedText)
-                        .font(.system(size: 14))
-                        .foregroundStyle(AppTheme.textPrimary)
-                        .frame(maxWidth: .infinity, alignment: .topLeading)
-                        .multilineTextAlignment(.leading)
-                        .padding(20)
+                    HighlightedCorrectedText(
+                        originalText: viewModel.text,
+                        correctedText: viewModel.correctedText,
+                        corrections: viewModel.issues.map(\.asTextCorrection)
+                    )
                 }
                 .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
             }

+ 6 - 6
gramora/Views/PunctuationCheckerView.swift

@@ -228,12 +228,12 @@ struct PunctuationCheckerView: View {
             }
 
             ScrollView(.vertical, showsIndicators: true) {
-                Text(viewModel.correctedText)
-                    .font(.system(size: 14))
-                    .foregroundStyle(AppTheme.textPrimary)
-                    .frame(maxWidth: .infinity, alignment: .leading)
-                    .textSelection(.enabled)
-                    .padding(16)
+                HighlightedCorrectedText(
+                    originalText: viewModel.text,
+                    correctedText: viewModel.correctedText,
+                    corrections: viewModel.issues.map(\.asTextCorrection),
+                    padding: 16
+                )
             }
             .frame(maxHeight: .infinity, alignment: .top)
             .background(AppTheme.tealLight.opacity(0.35))

+ 5 - 6
gramora/Views/SpellCheckerView.swift

@@ -228,12 +228,11 @@ struct SpellCheckerView: View {
 
             VStack(alignment: .leading, spacing: 0) {
                 ScrollView(.vertical, showsIndicators: true) {
-                    Text(viewModel.correctedText)
-                        .font(.system(size: 14))
-                        .foregroundStyle(AppTheme.textPrimary)
-                        .frame(maxWidth: .infinity, alignment: .topLeading)
-                        .multilineTextAlignment(.leading)
-                        .padding(20)
+                    HighlightedCorrectedText(
+                        originalText: viewModel.text,
+                        correctedText: viewModel.correctedText,
+                        corrections: viewModel.mistakes.map(\.asTextCorrection)
+                    )
                 }
                 .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
             }