18a0898741
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
105 lines
3.6 KiB
Swift
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)
|
|
}
|
|
}
|
|
}
|