2575 lines · 81.0 KB · Swift
RawDownload
import ExpoModulesCoreimport UIKit private struct ReviewDiffNativeRow: Decodable, Sendable {  let kind: String  let id: String  let fileId: String?  let filePath: String?  let previousPath: String?  let changeType: String?  let additions: Int?  let deletions: Int?  let text: String?  let content: String?  let change: String?  let oldLineNumber: Int?  let newLineNumber: Int?  let wordDiffRanges: [ReviewDiffNativeWordDiffRange]?  let commentText: String?  let commentRangeLabel: String?  let commentSectionTitle: String?} private struct ReviewDiffNativeWordDiffRange: Decodable, Sendable {  let start: Int  let end: Int} private struct ReviewDiffNativeToken: Decodable, Sendable {  let content: String  let color: String?  let fontStyle: Int?} private struct ReviewDiffNativeTokenPatch: Decodable, Sendable {  let resetKey: String?  let chunkIndex: Int?  let tokensByRowId: [String: [ReviewDiffNativeToken]]?} private struct ReviewDiffNativeThemePayload: Decodable {  let background: String?  let text: String?  let mutedText: String?  let headerBackground: String?  let border: String?  let hunkBackground: String?  let hunkText: String?  let addBackground: String?  let deleteBackground: String?  let addBar: String?  let deleteBar: String?  let addText: String?  let deleteText: String?} private struct ReviewDiffNativeTheme {  let background: UIColor  let text: UIColor  let mutedText: UIColor  let headerBackground: UIColor  let border: UIColor  let hunkBackground: UIColor  let hunkText: UIColor  let addBackground: UIColor  let deleteBackground: UIColor  let addBar: UIColor  let deleteBar: UIColor  let addText: UIColor  let deleteText: UIColor   static func resolve(_ scheme: String) -> ReviewDiffNativeTheme {    resolve(scheme, payload: nil)  }   static func resolve(    _ scheme: String,    payload: ReviewDiffNativeThemePayload?  ) -> ReviewDiffNativeTheme {    let fallback = fallback(scheme)    guard let payload else {      return fallback    }     return ReviewDiffNativeTheme(      background: UIColor(reviewDiffHex: payload.background) ?? fallback.background,      text: UIColor(reviewDiffHex: payload.text) ?? fallback.text,      mutedText: UIColor(reviewDiffHex: payload.mutedText) ?? fallback.mutedText,      headerBackground: UIColor(reviewDiffHex: payload.headerBackground) ?? fallback.headerBackground,      border: UIColor(reviewDiffHex: payload.border) ?? fallback.border,      hunkBackground: UIColor(reviewDiffHex: payload.hunkBackground) ?? fallback.hunkBackground,      hunkText: UIColor(reviewDiffHex: payload.hunkText) ?? fallback.hunkText,      addBackground: UIColor(reviewDiffHex: payload.addBackground) ?? fallback.addBackground,      deleteBackground: UIColor(reviewDiffHex: payload.deleteBackground) ?? fallback.deleteBackground,      addBar: UIColor(reviewDiffHex: payload.addBar) ?? fallback.addBar,      deleteBar: UIColor(reviewDiffHex: payload.deleteBar) ?? fallback.deleteBar,      addText: UIColor(reviewDiffHex: payload.addText) ?? fallback.addText,      deleteText: UIColor(reviewDiffHex: payload.deleteText) ?? fallback.deleteText    )  }   private static func fallback(_ scheme: String) -> ReviewDiffNativeTheme {    if scheme == "dark" {      return ReviewDiffNativeTheme(        background: UIColor(red: 0.07, green: 0.07, blue: 0.07, alpha: 1),        text: UIColor(red: 0.90, green: 0.90, blue: 0.90, alpha: 1),        mutedText: UIColor(red: 0.52, green: 0.52, blue: 0.52, alpha: 1),        headerBackground: UIColor(red: 0.10, green: 0.10, blue: 0.10, alpha: 1),        border: UIColor(red: 0.16, green: 0.16, blue: 0.16, alpha: 1),        hunkBackground: UIColor(red: 0.03, green: 0.14, blue: 0.18, alpha: 1),        hunkText: UIColor(red: 0.41, green: 0.82, blue: 1.00, alpha: 1),        addBackground: UIColor(red: 0.02, green: 0.16, blue: 0.10, alpha: 1),        deleteBackground: UIColor(red: 0.18, green: 0.05, blue: 0.08, alpha: 1),        addBar: UIColor(red: 0.02, green: 0.82, blue: 0.54, alpha: 1),        deleteBar: UIColor(red: 1.00, green: 0.36, blue: 0.50, alpha: 1),        addText: UIColor(red: 0.10, green: 0.84, blue: 0.56, alpha: 1),        deleteText: UIColor(red: 1.00, green: 0.38, blue: 0.52, alpha: 1)      )    }     return ReviewDiffNativeTheme(      background: UIColor.white,      text: UIColor(red: 0.16, green: 0.16, blue: 0.16, alpha: 1),      mutedText: UIColor(red: 0.47, green: 0.47, blue: 0.47, alpha: 1),      headerBackground: UIColor.white,      border: UIColor(red: 0.88, green: 0.88, blue: 0.90, alpha: 1),      hunkBackground: UIColor(red: 0.83, green: 0.92, blue: 0.98, alpha: 1),      hunkText: UIColor(red: 0.00, green: 0.45, blue: 0.74, alpha: 1),      addBackground: UIColor(red: 0.85, green: 0.94, blue: 0.92, alpha: 1),      deleteBackground: UIColor(red: 0.96, green: 0.85, blue: 0.90, alpha: 1),      addBar: UIColor(red: 0.02, green: 0.80, blue: 0.52, alpha: 1),      deleteBar: UIColor(red: 1.00, green: 0.34, blue: 0.48, alpha: 1),      addText: UIColor(red: 0.00, green: 0.56, blue: 0.34, alpha: 1),      deleteText: UIColor(red: 0.90, green: 0.10, blue: 0.24, alpha: 1)    )  }} private struct ReviewDiffNativeStylePayload: Decodable {  let rowHeight: Double?  let contentWidth: Double?  let changeBarWidth: Double?  let gutterWidth: Double?  let codePadding: Double?  let textVerticalInset: Double?  let fileHeaderHeight: Double?  let fileHeaderHorizontalMargin: Double?  let fileHeaderVerticalMargin: Double?  let fileHeaderCornerRadius: Double?  let fileHeaderHorizontalPadding: Double?  let fileHeaderPathRightPadding: Double?  let fileHeaderCountColumnWidth: Double?  let fileHeaderCountGap: Double?  let codeFontSize: Double?  let codeFontWeight: String?  let lineNumberFontSize: Double?  let lineNumberFontWeight: String?  let hunkFontSize: Double?  let hunkFontWeight: String?  let fileHeaderFontSize: Double?  let fileHeaderFontWeight: String?  let fileHeaderMetaFontSize: Double?  let fileHeaderMetaFontWeight: String?  let fileHeaderSubtextFontSize: Double?  let fileHeaderSubtextFontWeight: String?  let fileHeaderStatusFontSize: Double?  let fileHeaderStatusFontWeight: String?  let emptyStateFontSize: Double?  let emptyStateFontWeight: String?} private struct ReviewDiffNativeStyle {  let rowHeight: CGFloat  let contentWidth: CGFloat  let changeBarWidth: CGFloat  let gutterWidth: CGFloat  let codePadding: CGFloat  let textVerticalInset: CGFloat  let fileHeaderHeight: CGFloat  let fileHeaderHorizontalMargin: CGFloat  let fileHeaderVerticalMargin: CGFloat  let fileHeaderCornerRadius: CGFloat  let fileHeaderHorizontalPadding: CGFloat  let fileHeaderPathRightPadding: CGFloat  let fileHeaderCountColumnWidth: CGFloat  let fileHeaderCountGap: CGFloat  let codeFontSize: CGFloat  let codeFontWeight: UIFont.Weight  let lineNumberFontSize: CGFloat  let lineNumberFontWeight: UIFont.Weight  let hunkFontSize: CGFloat  let hunkFontWeight: UIFont.Weight  let fileHeaderFontSize: CGFloat  let fileHeaderFontWeight: UIFont.Weight  let fileHeaderMetaFontSize: CGFloat  let fileHeaderMetaFontWeight: UIFont.Weight  let fileHeaderSubtextFontSize: CGFloat  let fileHeaderSubtextFontWeight: UIFont.Weight  let fileHeaderStatusFontSize: CGFloat  let fileHeaderStatusFontWeight: UIFont.Weight  let emptyStateFontSize: CGFloat  let emptyStateFontWeight: UIFont.Weight   static func resolve(_ payload: ReviewDiffNativeStylePayload?) -> ReviewDiffNativeStyle {    ReviewDiffNativeStyle(      rowHeight: metric(payload?.rowHeight, fallback: 24),      contentWidth: metric(payload?.contentWidth, fallback: 2800),      changeBarWidth: nonNegativeMetric(payload?.changeBarWidth, fallback: 4),      gutterWidth: metric(payload?.gutterWidth, fallback: 50),      codePadding: metric(payload?.codePadding, fallback: 8),      textVerticalInset: metric(payload?.textVerticalInset, fallback: 3),      fileHeaderHeight: metric(payload?.fileHeaderHeight, fallback: 54),      fileHeaderHorizontalMargin: metric(payload?.fileHeaderHorizontalMargin, fallback: 8),      fileHeaderVerticalMargin: metric(payload?.fileHeaderVerticalMargin, fallback: 6),      fileHeaderCornerRadius: metric(payload?.fileHeaderCornerRadius, fallback: 10),      fileHeaderHorizontalPadding: metric(payload?.fileHeaderHorizontalPadding, fallback: 12),      fileHeaderPathRightPadding: metric(payload?.fileHeaderPathRightPadding, fallback: 128),      fileHeaderCountColumnWidth: metric(payload?.fileHeaderCountColumnWidth, fallback: 42),      fileHeaderCountGap: metric(payload?.fileHeaderCountGap, fallback: 6),      codeFontSize: metric(payload?.codeFontSize, fallback: 12),      codeFontWeight: fontWeight(payload?.codeFontWeight, fallback: .bold),      lineNumberFontSize: metric(payload?.lineNumberFontSize, fallback: 11),      lineNumberFontWeight: fontWeight(payload?.lineNumberFontWeight, fallback: .bold),      hunkFontSize: metric(payload?.hunkFontSize, fallback: 12),      hunkFontWeight: fontWeight(payload?.hunkFontWeight, fallback: .bold),      fileHeaderFontSize: metric(payload?.fileHeaderFontSize, fallback: 13),      fileHeaderFontWeight: fontWeight(payload?.fileHeaderFontWeight, fallback: .bold),      fileHeaderMetaFontSize: metric(payload?.fileHeaderMetaFontSize, fallback: 12),      fileHeaderMetaFontWeight: fontWeight(payload?.fileHeaderMetaFontWeight, fallback: .bold),      fileHeaderSubtextFontSize: metric(payload?.fileHeaderSubtextFontSize, fallback: 11),      fileHeaderSubtextFontWeight: fontWeight(payload?.fileHeaderSubtextFontWeight, fallback: .medium),      fileHeaderStatusFontSize: metric(payload?.fileHeaderStatusFontSize, fallback: 10),      fileHeaderStatusFontWeight: fontWeight(payload?.fileHeaderStatusFontWeight, fallback: .semibold),      emptyStateFontSize: metric(payload?.emptyStateFontSize, fallback: 13),      emptyStateFontWeight: fontWeight(payload?.emptyStateFontWeight, fallback: .semibold)    )  }   private static func metric(_ value: Double?, fallback: CGFloat) -> CGFloat {    guard let value, value.isFinite, value > 0 else {      return fallback    }    return CGFloat(value)  }   private static func nonNegativeMetric(_ value: Double?, fallback: CGFloat) -> CGFloat {    guard let value, value.isFinite, value >= 0 else {      return fallback    }    return CGFloat(value)  }   private static func fontWeight(_ value: String?, fallback: UIFont.Weight) -> UIFont.Weight {    switch value?.lowercased() {    case "ultralight", "ultra-light":      return .ultraLight    case "thin":      return .thin    case "light":      return .light    case "regular":      return .regular    case "medium":      return .medium    case "semibold", "semi-bold":      return .semibold    case "bold":      return .bold    case "heavy":      return .heavy    case "black":      return .black    default:      return fallback    }  }   func applyingOverrides(rowHeight: CGFloat?, contentWidth: CGFloat?) -> ReviewDiffNativeStyle {    ReviewDiffNativeStyle(      rowHeight: rowHeight ?? self.rowHeight,      contentWidth: contentWidth ?? self.contentWidth,      changeBarWidth: changeBarWidth,      gutterWidth: gutterWidth,      codePadding: codePadding,      textVerticalInset: textVerticalInset,      fileHeaderHeight: fileHeaderHeight,      fileHeaderHorizontalMargin: fileHeaderHorizontalMargin,      fileHeaderVerticalMargin: fileHeaderVerticalMargin,      fileHeaderCornerRadius: fileHeaderCornerRadius,      fileHeaderHorizontalPadding: fileHeaderHorizontalPadding,      fileHeaderPathRightPadding: fileHeaderPathRightPadding,      fileHeaderCountColumnWidth: fileHeaderCountColumnWidth,      fileHeaderCountGap: fileHeaderCountGap,      codeFontSize: codeFontSize,      codeFontWeight: codeFontWeight,      lineNumberFontSize: lineNumberFontSize,      lineNumberFontWeight: lineNumberFontWeight,      hunkFontSize: hunkFontSize,      hunkFontWeight: hunkFontWeight,      fileHeaderFontSize: fileHeaderFontSize,      fileHeaderFontWeight: fileHeaderFontWeight,      fileHeaderMetaFontSize: fileHeaderMetaFontSize,      fileHeaderMetaFontWeight: fileHeaderMetaFontWeight,      fileHeaderSubtextFontSize: fileHeaderSubtextFontSize,      fileHeaderSubtextFontWeight: fileHeaderSubtextFontWeight,      fileHeaderStatusFontSize: fileHeaderStatusFontSize,      fileHeaderStatusFontWeight: fileHeaderStatusFontWeight,      emptyStateFontSize: emptyStateFontSize,      emptyStateFontWeight: emptyStateFontWeight    )  }} public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate {  private let payloadDecodeQueue = DispatchQueue(    label: "com.t3tools.review-diff.payload-decode",    qos: .userInitiated  )  private let scrollView = UIScrollView()  private let contentView = ReviewDiffContentView()  private var rows: [ReviewDiffNativeRow] = []  private var appearanceScheme: String = "light"  private var themePayload: ReviewDiffNativeThemePayload?  private var stylePayload: ReviewDiffNativeStylePayload?  private var rowHeightOverride: CGFloat?  private var contentWidthOverride: CGFloat?  private var lastMetricsDebugKey = ""  private var lastVisibleRangeDebugKey = ""  private var tokensResetKey = ""  private var contentResetKey = ""  private var initialRowIndex: Int?  private var hasAppliedInitialRowIndex = false  private var lastVisibleFileId: String?  private var pendingScrollFileId: String?  private var pendingScrollAnimated = false  private var isProgrammaticScrollActive = false  private var rowsDecodeGeneration = 0  private var tokensDecodeGeneration = 0   let onDebug = EventDispatcher()  let onVisibleFileChange = EventDispatcher()  let onToggleFile = EventDispatcher()  let onToggleViewedFile = EventDispatcher()  let onPressLine = EventDispatcher()  let onToggleComment = EventDispatcher()  let onPullToRefresh = EventDispatcher()   private let pullRefreshControl = UIRefreshControl()   public required init(appContext: AppContext? = nil) {    super.init(appContext: appContext)     clipsToBounds = true    backgroundColor = contentView.theme.background    scrollView.contentInsetAdjustmentBehavior = .never    scrollView.delegate = self    scrollView.clipsToBounds = true    scrollView.alwaysBounceVertical = true    scrollView.alwaysBounceHorizontal = false    scrollView.showsVerticalScrollIndicator = true    scrollView.showsHorizontalScrollIndicator = false    scrollView.backgroundColor = contentView.theme.background    pullRefreshControl.addTarget(self, action: #selector(handlePullToRefresh), for: .valueChanged)    scrollView.refreshControl = pullRefreshControl    addSubview(scrollView)     contentView.backgroundColor = contentView.theme.background    contentView.onToggleFile = { [weak self] fileId in      self?.onToggleFile(["fileId": fileId])    }    contentView.onToggleViewedFile = { [weak self] fileId in      self?.onToggleViewedFile(["fileId": fileId])    }    contentView.onPressLine = { [weak self] payload in      self?.onPressLine(payload)    }    contentView.onToggleComment = { [weak self] commentId in      self?.onToggleComment(["commentId": commentId])    }    contentView.onDrawMetrics = { [weak self] metrics in      self?.emitDebug("draw-metrics", metrics)    }    scrollView.addSubview(contentView)  }   public override func layoutSubviews() {    super.layoutSubviews()    scrollView.frame = bounds    updateContentMetrics()  }   public func scrollViewDidScroll(_ scrollView: UIScrollView) {    if scrollView.isTracking || scrollView.isDragging || scrollView.isDecelerating {      contentView.isVerticalScrollActive = true    }    updateViewportFrame()  }   public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {    // A direct gesture takes ownership from any interrupted programmatic jump.    // Resume visible-file events immediately so the inspector follows the finger.    isProgrammaticScrollActive = false    contentView.isVerticalScrollActive = true  }   public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {    guard !decelerate else {      return    }     finishVerticalScroll()  }   public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {    finishVerticalScroll()  }   public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {    finishVerticalScroll()  }   func setRowsJson(_ rowsJson: String) {    rowsDecodeGeneration += 1    let generation = rowsDecodeGeneration     payloadDecodeQueue.async { [weak self] in      guard let data = rowsJson.data(using: .utf8) else {        return      }       do {        let decodedRows = try JSONDecoder().decode([ReviewDiffNativeRow].self, from: data)        DispatchQueue.main.async { [weak self] in          guard let self, generation == self.rowsDecodeGeneration else {            return          }          self.rows = decodedRows          self.contentView.rows = decodedRows          self.hasAppliedInitialRowIndex = false          self.lastVisibleFileId = nil          self.emitDebug("rows-decoded", [            "rows": decodedRows.count,            "firstKind": decodedRows.first?.kind ?? "none",          ])          self.updateContentMetrics()          self.applyPendingScrollIfNeeded()        }      } catch {        let message = error.localizedDescription        DispatchQueue.main.async { [weak self] in          guard let self, generation == self.rowsDecodeGeneration else {            return          }          self.rows = []          self.contentView.rows = []          self.hasAppliedInitialRowIndex = false          self.lastVisibleFileId = nil          self.pendingScrollFileId = nil          self.updateContentMetrics()          self.emitDebug("rows-decode-failed", ["error": message])        }      }    }  }   func setTokensJson(_ tokensJson: String) {    tokensDecodeGeneration += 1    let generation = tokensDecodeGeneration     payloadDecodeQueue.async { [weak self] in      guard let data = tokensJson.data(using: .utf8) else {        return      }       do {        let decodedTokens = try JSONDecoder().decode(          [String: [ReviewDiffNativeToken]].self,          from: data        )        DispatchQueue.main.async { [weak self] in          guard let self, generation == self.tokensDecodeGeneration else {            return          }          self.contentView.tokensByRowId = decodedTokens        }      } catch {        let message = error.localizedDescription        DispatchQueue.main.async { [weak self] in          guard let self, generation == self.tokensDecodeGeneration else {            return          }          self.contentView.tokensByRowId = [:]          self.emitDebug("tokens-decode-failed", ["error": message])        }      }    }  }   func setTokensPatchJson(_ tokensPatchJson: String) {    let contentResetKey = self.contentResetKey

Lines 1–500 of 2575.