Files
kotej/App/TextCompareView.swift
alexandrev-tibco 18a0898741 Compare pasted text without any files
Checking two blobs of JSON meant saving them to disk first, which is friction for
the most common quick comparison there is. A tab can now be a pair of editors:
paste on each side and the diff appears underneath.

It goes through the same engine as files, so JSON and XML are still compared
structurally — the kind is detected from the pasted content itself, since there's
no filename to go on. Reachable from the empty state or cmd-shift-T.

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

105 lines
3.6 KiB
Swift

import SwiftUI
import KotejEngine
/// Compare two snippets without touching the filesystem paste on each side and
/// see the diff. The most common quick comparison there is, and needing to save
/// files first just to check two blobs of JSON is friction.
struct TextCompareView: View {
@Bindable var model: ComparisonModel
var body: some View {
VStack(spacing: 0) {
editors
Divider()
summary
Divider()
diff
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
private var editors: some View {
HStack(spacing: 0) {
editor(title: "Left", text: $model.leftText)
Divider()
editor(title: "Right", text: $model.rightText)
}
.frame(minHeight: 140, idealHeight: 190)
}
private func editor(title: LocalizedStringKey, text: Binding<String>) -> some View {
VStack(alignment: .leading, spacing: 0) {
HStack {
Text(title).font(.caption.weight(.semibold)).foregroundStyle(.secondary)
Spacer()
Button {
text.wrappedValue = ""
} label: {
Image(systemName: "xmark.circle").font(.caption)
}
.buttonStyle(.borderless)
.disabled(text.wrappedValue.isEmpty)
.help("Clear")
}
.padding(.horizontal, 10)
.padding(.vertical, 4)
TextEditor(text: text)
.font(.system(.caption, design: .monospaced))
.scrollContentBackground(.hidden)
.background(Color(nsColor: .textBackgroundColor))
.overlay(alignment: .topLeading) {
if text.wrappedValue.isEmpty {
Text("Paste here…")
.font(.system(.caption, design: .monospaced))
.foregroundStyle(.tertiary)
.padding(.horizontal, 5)
.padding(.vertical, 8)
.allowsHitTesting(false)
}
}
}
.frame(maxWidth: .infinity)
}
/// The verdict, using the same engine as files: JSON and XML are compared
/// structurally here too.
private var summary: some View {
HStack(spacing: 10) {
if model.leftText.isEmpty && model.rightText.isEmpty {
Text("Paste something on each side to compare.")
.font(.caption).foregroundStyle(.secondary)
} else {
StatusBadge(status: model.textStatus)
ModeIcon(mode: model.textMode)
Text(verbatim: model.textKindLabel)
.font(.caption).foregroundStyle(.secondary)
}
Spacer()
}
.padding(.horizontal, 12)
.padding(.vertical, 5)
.background(.bar)
}
@ViewBuilder private var diff: some View {
let rows = model.textRows
if rows.isEmpty {
Spacer()
Text("No differences").foregroundStyle(.secondary)
Spacer()
} else {
ScrollView(.vertical) {
LazyVStack(alignment: .leading, spacing: 0) {
ForEach(Array(rows.enumerated()), id: \.offset) { _, row in
DiffLine(row: row, wrapLines: true)
}
}
.padding(.vertical, 4)
.frame(maxWidth: .infinity, alignment: .topLeading)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
}
}