PadXcode-iPad/Editor/FindInFileView.swift

74 lines
4.1 KiB
Swift
Raw Normal View History

2026-04-12 00:46:30 -07:00
import SwiftUI
import Foundation
struct FindInFileView: View {
let content: String
let onNavigate: (NSRange) -> Void
@Environment(\.dismiss) var dismiss
@State private var query = ""; @State private var isRegex = false; @State private var isCaseSensitive = false
@State private var results: [Result] = []; @State private var selected = 0
@FocusState private var focused: Bool
struct Result: Identifiable {
let id = UUID(); let range: NSRange; let lineNumber: Int; let lineText: String
}
var body: some View {
NavigationStack {
VStack(spacing: 0) {
HStack(spacing: 8) {
Image(systemName: "magnifyingglass").foregroundStyle(.secondary)
TextField("Find…", text: $query).textFieldStyle(.plain).font(.system(.body, design: .monospaced))
.autocorrectionDisabled().autocapitalization(.none).focused($focused).onSubmit { navigateNext() }
.onChange(of: query) { _, _ in runSearch() }
if !results.isEmpty {
Text("\(selected+1)/\(results.count)").font(.system(.caption, design: .monospaced)).foregroundStyle(.secondary)
Button { navigatePrev() } label: { Image(systemName: "chevron.up") }.buttonStyle(.bordered).controlSize(.small)
Button { navigateNext() } label: { Image(systemName: "chevron.down") }.buttonStyle(.bordered).controlSize(.small)
}
}
.padding(.horizontal, 14).padding(.vertical, 10).background(Color(.secondarySystemBackground))
Divider()
if results.isEmpty {
ContentUnavailableView(query.isEmpty ? "Type to search" : "No Results", systemImage: "magnifyingglass")
} else {
List(Array(results.enumerated()), id: \.element.id) { idx, r in
HStack(alignment: .top, spacing: 10) {
Text("\(r.lineNumber)").font(.system(.caption, design: .monospaced)).foregroundStyle(.secondary).frame(width: 36, alignment: .trailing)
Text(r.lineText).font(.system(.body, design: .monospaced)).lineLimit(1)
}
.background(idx == selected ? Color.accentColor.opacity(0.1) : .clear)
.onTapGesture { selected = idx; onNavigate(r.range); dismiss() }
}
.listStyle(.plain)
}
}
.navigationTitle("Find in File").navigationBarTitleDisplayMode(.inline)
.toolbar { ToolbarItem(placement: .cancellationAction) { Button("Done") { dismiss() } } }
.onAppear { focused = true }
}
.presentationDetents([.medium, .large])
}
private func runSearch() {
guard !query.isEmpty else { results = []; return }
let lines = content.components(separatedBy: "\n")
var found: [Result] = []; var offset = 0
let pattern = isRegex ? query : NSRegularExpression.escapedPattern(for: query)
let options: NSRegularExpression.Options = isCaseSensitive ? [] : .caseInsensitive
guard let regex = try? NSRegularExpression(pattern: pattern, options: options) else { results = []; return }
for (i, line) in lines.enumerated() {
let r = NSRange(line.startIndex..., in: line)
for m in regex.matches(in: line, range: r) {
found.append(Result(range: NSRange(location: offset + m.range.location, length: m.range.length),
lineNumber: i + 1, lineText: line.trimmingCharacters(in: .whitespaces)))
}
offset += line.count + 1
}
2026-04-12 22:07:35 -07:00
results = found; if !found.isEmpty { selected = 0 }
2026-04-12 00:46:30 -07:00
}
private func navigateNext() { guard !results.isEmpty else { return }; selected = (selected+1) % results.count; onNavigate(results[selected].range) }
private func navigatePrev() { guard !results.isEmpty else { return }; selected = (selected-1+results.count) % results.count; onNavigate(results[selected].range) }
}