Files
kotej/App/FileDiffView.swift
T
alexandrev-tibco f85c746af0 Rename to Kotej and add the app icon
"Cotejo" means nothing outside Spanish and is hard to pronounce elsewhere, so the
project is now Kotej: same root, but it reads and sounds the same in English and
Spanish. Renamed throughout — engine, targets, bundle ids and the URL scheme,
which is now kotej://compare.

Icon generated with Codex: two facing panels split by a seam, content lines on
each side and one amber line marking a difference — the tool's job at a glance,
still legible at 16pt.

34 tests still green; kotej:// verified end to end against the demo JARs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UfhDYRLTpGJSKGP1m3LVJN
2026-07-27 17:24:05 +02:00

159 lines
5.2 KiB
Swift

import SwiftUI
import KotejEngine
/// Side-by-side contents of the selected row. Text (and JSON/XML) is aligned line
/// by line; binaries just report their verdict, since a hex view would be noise.
struct FileDiffView: View {
@Bindable var model: ComparisonModel
@State private var rows: [TextDiff.Row] = []
@State private var notice: String?
@State private var isLoading = false
var body: some View {
VStack(spacing: 0) {
if let node = model.selection {
header(node)
Divider()
content(node)
} else {
Spacer()
Text("Select a file to see its differences")
.foregroundStyle(.secondary)
Spacer()
}
}
.task(id: model.selection?.path) { await load() }
}
private func header(_ node: DiffNode) -> some View {
HStack {
Image(systemName: node.isDirectory ? "folder.fill" : "doc.text")
Text(verbatim: node.path.isEmpty ? node.name : node.path)
.font(.callout.weight(.medium))
.lineLimit(1)
.truncationMode(.head)
Spacer()
if isLoading { ProgressView().controlSize(.small) }
}
.padding(.horizontal, 12)
.padding(.vertical, 6)
}
@ViewBuilder private func content(_ node: DiffNode) -> some View {
if let notice {
Spacer()
Text(verbatim: notice).foregroundStyle(.secondary).padding()
Spacer()
} else if rows.isEmpty {
Spacer()
Group { if !isLoading { Text("Nothing to show") } }.foregroundStyle(.secondary)
Spacer()
} else {
ScrollView([.vertical, .horizontal]) {
LazyVStack(alignment: .leading, spacing: 0) {
ForEach(Array(rows.enumerated()), id: \.offset) { _, row in
DiffLine(row: row)
}
}
.padding(.vertical, 4)
}
}
}
private func load() async {
rows = []
notice = nil
guard let node = model.selection else { return }
if node.isDirectory {
notice = String(localized: node.isArchive
? "Archive: expand it in the tree to see its contents."
: "Folder: select a file inside it.")
return
}
guard let left = node.left, let right = node.right else {
notice = String(localized: node.left == nil ? "Only exists on the right." : "Only exists on the left.")
return
}
isLoading = true
defer { isLoading = false }
let loaded: (Data, Data)? = await Task.detached(priority: .userInitiated) {
guard let l = try? left.data(), let r = try? right.data() else { return nil }
return (l, r)
}.value
guard let (leftData, rightData) = loaded else {
notice = String(localized: "Couldn't read the contents.")
return
}
let kind = ContentKind.detect(path: node.name, data: leftData)
guard kind != .binary else {
notice = leftData == rightData
? String(localized: "Identical binary (\(leftData.count) bytes).")
: String(localized: "Different binary (\(leftData.count) vs \(rightData.count) bytes).")
return
}
let leftText = String(decoding: leftData, as: UTF8.self)
let rightText = String(decoding: rightData, as: UTF8.self)
rows = await Task.detached(priority: .userInitiated) {
TextDiff.rows(left: leftText, right: rightText)
}.value
if node.status == .equivalent {
notice = nil
}
}
}
/// One aligned line pair.
struct DiffLine: View {
let row: TextDiff.Row
var body: some View {
HStack(spacing: 0) {
cell(number: row.leftNumber, text: row.left, tint: leftTint)
Divider()
cell(number: row.rightNumber, text: row.right, tint: rightTint)
}
.font(.system(.caption, design: .monospaced))
}
private func cell(number: Int?, text: String, tint: Color) -> some View {
HStack(spacing: 8) {
Text(verbatim: number.map(String.init) ?? "")
.frame(width: 44, alignment: .trailing)
.foregroundStyle(.tertiary)
Text(verbatim: text.isEmpty ? " " : text)
.frame(maxWidth: .infinity, alignment: .leading)
.textSelection(.enabled)
}
.padding(.horizontal, 6)
.padding(.vertical, 1)
.frame(minWidth: 320, alignment: .leading)
.background(tint)
}
private var leftTint: Color {
switch row.kind {
case .equal: return .clear
case .changed: return .yellow.opacity(0.18)
case .removed: return .red.opacity(0.16)
case .added: return .secondary.opacity(0.06)
}
}
private var rightTint: Color {
switch row.kind {
case .equal: return .clear
case .changed: return .yellow.opacity(0.18)
case .added: return .green.opacity(0.16)
case .removed: return .secondary.opacity(0.06)
}
}
}